.dotfiles/.local/share/gnome-shell/extensions/dash-to-panel@jderose9.gith.../appIcons.js

1887 lines
72 KiB
JavaScript

/*
* This file is part of the Dash-To-Panel extension for Gnome 3
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* Credits:
* This file is based on code from the Dash to Dock extension by micheleg
* and code from the Taskbar extension by Zorin OS
* Some code was also adapted from the upstream Gnome Shell source code.
*/
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import Graphene from 'gi://Graphene';
import GObject from 'gi://GObject';
import Mtk from 'gi://Mtk';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
import * as AppMenu from 'resource:///org/gnome/shell/ui/appMenu.js';
import * as Dash from 'resource:///org/gnome/shell/ui/dash.js';
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import * as Util from 'resource:///org/gnome/shell/misc/util.js';
import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js';
import {EventEmitter} from 'resource:///org/gnome/shell/misc/signals.js';
import * as Utils from './utils.js';
import * as PanelSettings from './panelSettings.js';
import * as Taskbar from './taskbar.js';
import * as Progress from './progress.js';
import {DTP_EXTENSION, SETTINGS, DESKTOPSETTINGS, EXTENSION_PATH} from './extension.js';
import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
//timeout names
const T2 = 'mouseScrollTimeout';
const T3 = 'showDotsTimeout';
const T4 = 'overviewWindowDragEndTimeout';
const T5 = 'switchWorkspaceTimeout';
const T6 = 'displayProperIndicatorTimeout';
//right padding defined for .overview-label in stylesheet.css
const TITLE_RIGHT_PADDING = 8;
const DOUBLE_CLICK_DELAY_MS = 450;
let LABEL_GAP = 5;
let MAX_INDICATORS = 4;
export const DEFAULT_PADDING_SIZE = 4;
let DOT_STYLE = {
DOTS: "DOTS",
SQUARES: "SQUARES",
DASHES: "DASHES",
SEGMENTED: "SEGMENTED",
CILIORA: "CILIORA",
METRO: "METRO",
SOLID: "SOLID"
}
let DOT_POSITION = {
TOP: "TOP",
BOTTOM: "BOTTOM",
LEFT: 'LEFT',
RIGHT: 'RIGHT'
}
let recentlyClickedAppLoopId = 0;
let recentlyClickedApp = null;
let recentlyClickedAppWindows = null;
let recentlyClickedAppIndex = 0;
let recentlyClickedAppMonitorIndex;
let tracker = Shell.WindowTracker.get_default();
/**
* Extend AppIcon
*
* - Apply a css class based on the number of windows of each application (#N);
* - Draw a dot for each window of the application based on the default "dot" style which is hidden (#N);
* a class of the form "running#N" is applied to the AppWellIcon actor.
* like the original .running one.
* - add a .focused style to the focused app
* - Customize click actions.
* - Update minimization animation target
*
*/
export const TaskbarAppIcon = GObject.registerClass({
}, class TaskbarAppIcon extends AppDisplay.AppIcon {
_init(appInfo, panel, iconParams, previewMenu, iconAnimator) {
this.dtpPanel = panel;
this._nWindows = 0;
this.window = appInfo.window;
this.isLauncher = appInfo.isLauncher;
this._previewMenu = previewMenu;
this.iconAnimator = iconAnimator;
this.lastClick = 0;
super._init(appInfo.app, iconParams);
this._timeoutsHandler = new Utils.TimeoutsHandler();
// Fix touchscreen issues before the listener is added by the parent constructor.
this._onTouchEvent = function(actor, event) {
if (event.type() == Clutter.EventType.TOUCH_BEGIN) {
// Open the popup menu on long press.
this._setPopupTimeout();
} else if (this._menuTimeoutId != 0 && (event.type() == Clutter.EventType.TOUCH_END || event.type() == Clutter.EventType.TOUCH_CANCEL)) {
// Activate/launch the application.
this.activate(1);
this._removeMenuTimeout();
}
// Disable dragging via touch screen as it's buggy as hell. Not perfect for tablet users, but the alternative is way worse.
// Also, EVENT_PROPAGATE launches applications twice with this solution, so this.activate(1) above must only be called if there's already a window.
return Clutter.EVENT_STOP;
};
// Hack for missing TOUCH_END event.
this._onLeaveEvent = function(actor, event) {
this.fake_release();
if (this._menuTimeoutId != 0) this.activate(1); // Activate/launch the application if TOUCH_END didn't fire.
this._removeMenuTimeout();
};
this._dot.set_width(0);
this._isGroupApps = SETTINGS.get_boolean('group-apps');
this._container = new St.Widget({ style_class: 'dtp-container', layout_manager: new Clutter.BinLayout() });
this._dotsContainer = new St.Widget({ layout_manager: new Clutter.BinLayout() });
this._dtpIconContainer = new St.Widget({ layout_manager: new Clutter.BinLayout(), style: getIconContainerStyle(panel.checkIfVertical()) });
this.remove_actor(this._iconContainer);
this._dtpIconContainer.add_child(this._iconContainer);
if (appInfo.window) {
let box = new St.BoxLayout();
this._windowTitle = new St.Label({
y_align: Clutter.ActorAlign.CENTER,
x_align: Clutter.ActorAlign.START,
style_class: 'overview-label'
});
this._updateWindowTitle();
this._updateWindowTitleStyle();
this._scaleFactorChangedId = Utils.getStageTheme().connect('changed', () => this._updateWindowTitleStyle());
box.add_child(this._dtpIconContainer);
box.add_child(this._windowTitle);
this._dotsContainer.add_child(box);
} else {
this._dotsContainer.add_child(this._dtpIconContainer);
}
this._container.add_child(this._dotsContainer);
this.set_child(this._container);
if (panel.checkIfVertical()) {
this.set_width(panel.geom.w);
}
// Monitor windows-changes instead of app state.
// Keep using the same Id and function callback (that is extended)
if(this._stateChangedId > 0) {
this.app.disconnect(this._stateChangedId);
this._stateChangedId = 0;
}
this._onAnimateAppiconHoverChanged();
this._setAppIconPadding();
this._showDots();
this._focusWindowChangedId = global.display.connect('notify::focus-window',
this._onFocusAppChanged.bind(this));
this._windowEnteredMonitorId = this._windowLeftMonitorId = 0;
this._stateChangedId = this.app.connect('windows-changed', this.onWindowsChanged.bind(this));
if (!this.window) {
if (SETTINGS.get_boolean('isolate-monitors')) {
this._windowEnteredMonitorId = Utils.DisplayWrapper.getScreen().connect('window-entered-monitor', this.onWindowEnteredOrLeft.bind(this));
this._windowLeftMonitorId = Utils.DisplayWrapper.getScreen().connect('window-left-monitor', this.onWindowEnteredOrLeft.bind(this));
}
this._titleWindowChangeId = 0;
this._minimizedWindowChangeId = 0;
} else {
this._titleWindowChangeId = this.window.connect('notify::title',
this._updateWindowTitle.bind(this));
this._minimizedWindowChangeId = this.window.connect('notify::minimized',
this._updateWindowTitleStyle.bind(this));
}
this._scrollEventId = this.connect('scroll-event', this._onMouseScroll.bind(this));
this._overviewWindowDragEndId = Main.overview.connect('window-drag-end',
this._onOverviewWindowDragEnd.bind(this));
this._switchWorkspaceId = global.window_manager.connect('switch-workspace',
this._onSwitchWorkspace.bind(this));
this._hoverChangeId = this.connect('notify::hover', () => this._onAppIconHoverChanged());
this._dtpSettingsSignalIds = [
SETTINGS.connect('changed::animate-appicon-hover', this._onAnimateAppiconHoverChanged.bind(this)),
SETTINGS.connect('changed::dot-position', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-size', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-style-focused', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-style-unfocused', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-dominant', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-override', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-1', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-2', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-3', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-4', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-unfocused-different', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-unfocused-1', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-unfocused-2', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-unfocused-3', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::dot-color-unfocused-4', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::focus-highlight', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::focus-highlight-dominant', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::focus-highlight-color', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::focus-highlight-opacity', this._settingsChangeRefresh.bind(this)),
SETTINGS.connect('changed::group-apps-label-font-size', this._updateWindowTitleStyle.bind(this)),
SETTINGS.connect('changed::group-apps-label-font-weight', this._updateWindowTitleStyle.bind(this)),
SETTINGS.connect('changed::group-apps-label-font-color', this._updateWindowTitleStyle.bind(this)),
SETTINGS.connect('changed::group-apps-label-font-color-minimized', this._updateWindowTitleStyle.bind(this)),
SETTINGS.connect('changed::group-apps-label-max-width', this._updateWindowTitleStyle.bind(this)),
SETTINGS.connect('changed::group-apps-use-fixed-width', this._updateWindowTitleStyle.bind(this)),
SETTINGS.connect('changed::group-apps-underline-unfocused', this._settingsChangeRefresh.bind(this))
]
this._progressIndicator = new Progress.ProgressIndicator(this, panel.progressManager);
this._numberOverlay();
}
getDragActor() {
return this.app.create_icon_texture(this.dtpPanel.taskbar.iconSize);
}
// Used by TaskbarItemContainer to animate appIcons on hover
getCloneButton() {
// The source of the clone is this._container,
// using this.actor directly would break DnD style.
let clone = new Clutter.Clone({
source: this.child,
x: this.child.x, y: this.child.y,
width: this.child.width, height: this.child.height,
pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
opacity: 255,
reactive: false,
x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER,
});
clone._delegate = this._delegate;
// "clone" of this.actor
return new St.Button({
child: clone,
x: this.x, y: this.y,
width: this.width, height: this.height,
reactive: false,
});
}
shouldShowTooltip() {
if (!SETTINGS.get_boolean('show-tooltip') ||
(!this.isLauncher && SETTINGS.get_boolean("show-window-previews") &&
this.getAppIconInterestingWindows().length > 0)) {
return false;
} else {
return this.hover && !this.window &&
(!this._menu || !this._menu.isOpen) &&
(this._previewMenu.getCurrentAppIcon() !== this);
}
}
_onAppIconHoverChanged() {
if (!SETTINGS.get_boolean('show-window-previews') ||
(!this.window && !this._nWindows)) {
return;
}
if (this.hover) {
this._previewMenu.requestOpen(this);
} else {
this._previewMenu.requestClose();
}
}
_onDestroy() {
super._onDestroy();
this._timeoutsHandler.destroy();
this._previewMenu.close(true);
// Disconect global signals
if (this._stateChangedId > 0) {
this.app.disconnect(this._stateChangedId);
this._stateChangedId = 0;
}
if(this._overviewWindowDragEndId)
Main.overview.disconnect(this._overviewWindowDragEndId);
if(this._focusWindowChangedId)
global.display.disconnect(this._focusWindowChangedId);
if(this._titleWindowChangeId)
this.window.disconnect(this._titleWindowChangeId);
if(this._minimizedWindowChangeId)
this.window.disconnect(this._minimizedWindowChangeId);
if (this._windowEnteredMonitorId) {
Utils.DisplayWrapper.getScreen().disconnect(this._windowEnteredMonitorId);
Utils.DisplayWrapper.getScreen().disconnect(this._windowLeftMonitorId);
}
if(this._switchWorkspaceId)
global.window_manager.disconnect(this._switchWorkspaceId);
if(this._scaleFactorChangedId)
Utils.getStageTheme().disconnect(this._scaleFactorChangedId);
if (this._hoverChangeId) {
this.disconnect(this._hoverChangeId);
}
if (this._scrollEventId) {
this.disconnect(this._scrollEventId);
}
for (let i = 0; i < this._dtpSettingsSignalIds.length; ++i) {
SETTINGS.disconnect(this._dtpSettingsSignalIds[i]);
}
}
onWindowsChanged() {
this._updateWindows();
this.updateIcon();
if (this._isGroupApps)
this._setIconStyle();
}
onWindowEnteredOrLeft(display, number, metaWindow) {
if (number > 0 && tracker.get_window_app(metaWindow) == this.app) {
this._updateWindows();
this._displayProperIndicator();
}
}
updateTitleStyle() {
this._updateWindowTitleStyle();
}
// Update indicator and target for minimization animation
updateIcon() {
// If (for unknown reason) the actor is not on the stage the reported size
// and position are random values, which might exceeds the integer range
// resulting in an error when assigned to the a rect. This is a more like
// a workaround to prevent flooding the system with errors.
if (this.get_stage() == null)
return;
let rect = new Mtk.Rectangle();
[rect.x, rect.y] = this.get_transformed_position();
[rect.width, rect.height] = this.get_transformed_size();
let windows = this.window ? [this.window] : this.getAppIconInterestingWindows(true);
windows.forEach(function(w) {
w.set_icon_geometry(rect);
});
}
_onAnimateAppiconHoverChanged() {
if (SETTINGS.get_boolean('animate-appicon-hover')) {
this._container.add_style_class_name('animate-appicon-hover');
// Workaround to prevent scaled icon from being ugly when it is animated on hover.
// It increases the "resolution" of the icon without changing the icon size.
this.icon.createIcon = (iconSize) => this.app.create_icon_texture(2 * iconSize);
this._iconIconBinActorAddedId = this.icon._iconBin.connect('actor-added', () => {
let size = this.icon.iconSize * Utils.getScaleFactor()
if (this.icon._iconBin.child.mapped) {
this.icon._iconBin.child.set_size(size, size);
} else {
let iconMappedId = this.icon._iconBin.child.connect('notify::mapped', () => {
this.icon._iconBin.child.set_size(size, size);
this.icon._iconBin.child.disconnect(iconMappedId);
});
}
});
if (this.icon._iconBin.child)
this.icon._createIconTexture(this.icon.iconSize);
} else {
this._container.remove_style_class_name('animate-appicon-hover');
if (this._iconIconBinActorAddedId) {
this.icon._iconBin.disconnect(this._iconIconBinActorAddedId);
this._iconIconBinActorAddedId = 0;
this.icon.createIcon = this._createIcon.bind(this);
}
}
}
_onMouseScroll(actor, event) {
let scrollAction = SETTINGS.get_string('scroll-icon-action');
if (scrollAction === 'PASS_THROUGH') {
return this.dtpPanel._onPanelMouseScroll(actor, event);
} else if (scrollAction === 'NOTHING' || (!this.window && !this._nWindows)) {
return;
}
let direction = Utils.getMouseScrollDirection(event);
if (direction && !this._timeoutsHandler.getId(T2)) {
this._timeoutsHandler.add([T2, SETTINGS.get_int('scroll-icon-delay'), () => {}]);
let windows = this.getAppIconInterestingWindows();
windows.sort(Taskbar.sortWindowsCompareFunction);
Utils.activateSiblingWindow(windows, direction, this.window);
}
}
_showDots() {
// Just update style if dots already exist
if (this._focusedDots && this._unfocusedDots) {
this._updateWindows();
return;
}
if (!this._isGroupApps) {
this._focusedDots = new St.Widget({
layout_manager: new Clutter.BinLayout(),
x_expand: true, y_expand: true,
visible: false
});
let mappedId = this.connect('notify::mapped', () => {
this._displayProperIndicator();
this.disconnect(mappedId);
});
} else {
this._focusedDots = new St.DrawingArea(),
this._unfocusedDots = new St.DrawingArea();
this._focusedDots.connect('repaint', () => {
if (!this._dashItemContainer.animatingOut)
// don't draw and trigger more animations if the icon is in the middle of
// being removed from the panel
this._drawRunningIndicator(this._focusedDots, SETTINGS.get_string('dot-style-focused'), true);
});
this._unfocusedDots.connect('repaint', () => {
if (!this._dashItemContainer.animatingOut)
this._drawRunningIndicator(this._unfocusedDots, SETTINGS.get_string('dot-style-unfocused'), false);
});
this._dotsContainer.add_child(this._unfocusedDots);
this._updateWindows();
this._timeoutsHandler.add([T3, 0, () => {
this._resetDots();
this._displayProperIndicator();
}]);
}
this._dotsContainer.add_child(this._focusedDots);
}
_resetDots() {
let position = SETTINGS.get_string('dot-position');
let isHorizontalDots = position == DOT_POSITION.TOP || position == DOT_POSITION.BOTTOM;
let sizeProp = isHorizontalDots ? 'width' : 'height';
let focusedDotStyle = SETTINGS.get_string('dot-style-focused');
let unfocusedDotStyle = SETTINGS.get_string('dot-style-unfocused');
this._focusedIsWide = this._isWideDotStyle(focusedDotStyle);
this._unfocusedIsWide = this._isWideDotStyle(unfocusedDotStyle);
[, this._containerSize] = this._container[`get_preferred_${sizeProp}`](-1);
[this._focusedDots, this._unfocusedDots].forEach(d => {
d.set_size(-1, -1);
d.x_expand = d.y_expand = false;
d[sizeProp] = 1;
d[(isHorizontalDots ? 'y' : 'x') + '_expand'] = true;
});
}
_settingsChangeRefresh() {
if (this._isGroupApps) {
this._updateWindows();
this._resetDots();
this._focusedDots.queue_repaint();
this._unfocusedDots.queue_repaint();
}
this._displayProperIndicator();
}
_updateWindowTitleStyle() {
if (this._windowTitle) {
let useFixedWidth = SETTINGS.get_boolean('group-apps-use-fixed-width');
let fontWeight = SETTINGS.get_string('group-apps-label-font-weight');
let fontScale = DESKTOPSETTINGS.get_double('text-scaling-factor');
let fontColor = this.window.minimized ?
SETTINGS.get_string('group-apps-label-font-color-minimized') :
SETTINGS.get_string('group-apps-label-font-color');
let scaleFactor = Utils.getScaleFactor();
let maxLabelWidth = SETTINGS.get_int('group-apps-label-max-width') * scaleFactor;
let variableWidth = !useFixedWidth || this.dtpPanel.checkIfVertical() || this.dtpPanel.taskbar.fullScrollView;
this._windowTitle[(maxLabelWidth > 0 ? 'show' : 'hide')]();
this._windowTitle.set_width(variableWidth ? -1 : maxLabelWidth + TITLE_RIGHT_PADDING * scaleFactor);
this._windowTitle.clutter_text.natural_width = useFixedWidth ? maxLabelWidth : 0;
this._windowTitle.clutter_text.natural_width_set = useFixedWidth;
this._windowTitle.set_style('font-size: ' + SETTINGS.get_int('group-apps-label-font-size') * fontScale + 'px;' +
'font-weight: ' + fontWeight + ';' +
(useFixedWidth ? '' : 'max-width: ' + maxLabelWidth + 'px;') +
'color: ' + fontColor);
}
}
_updateWindowTitle() {
if (this._windowTitle.text != this.window.title) {
this._windowTitle.text = (this.window.title ? this.window.title : this.app.get_name()).replace(/\r?\n|\r/g, '').trim();
if (this._focusedDots) {
this._displayProperIndicator();
}
}
}
_setIconStyle(isFocused) {
let inlineStyle = 'margin: 0;';
if(SETTINGS.get_boolean('focus-highlight') &&
this._checkIfFocusedApp() && !this.isLauncher &&
(!this.window || isFocused) && !this._isThemeProvidingIndicator() && this._checkIfMonitorHasFocus()) {
let focusedDotStyle = SETTINGS.get_string('dot-style-focused');
let pos = SETTINGS.get_string('dot-position');
let highlightMargin = this._focusedIsWide ? SETTINGS.get_int('dot-size') : 0;
if(!this.window) {
let containerWidth = this._dtpIconContainer.get_width() / Utils.getScaleFactor();
let backgroundSize = containerWidth + "px " +
(containerWidth - (pos == DOT_POSITION.BOTTOM ? highlightMargin : 0)) + "px;";
if (focusedDotStyle == DOT_STYLE.CILIORA || focusedDotStyle == DOT_STYLE.SEGMENTED)
highlightMargin += 1;
if (this._nWindows > 1 && focusedDotStyle == DOT_STYLE.METRO) {
let bgSvg = '/img/highlight_stacked_bg';
if (pos == DOT_POSITION.LEFT || pos == DOT_POSITION.RIGHT) {
bgSvg += (this.dtpPanel.checkIfVertical() ? '_2' : '_3');
}
inlineStyle += "background-image: url('" + EXTENSION_PATH + bgSvg + ".svg');" +
"background-position: 0 " + (pos == DOT_POSITION.TOP ? highlightMargin : 0) + "px;" +
"background-size: " + backgroundSize;
}
}
let highlightColor = this._getFocusHighlightColor();
inlineStyle += "background-color: " + cssHexTocssRgba(highlightColor, SETTINGS.get_int('focus-highlight-opacity') * 0.01);
}
if(this._dotsContainer.get_style() != inlineStyle) {
this._dotsContainer.set_style(inlineStyle);
}
}
_checkIfFocusedApp() {
return tracker.focus_app == this.app;
}
_checkIfMonitorHasFocus() {
return global.display.focus_window &&
(!SETTINGS.get_boolean('multi-monitors') || // only check same monitor index if multi window is enabled.
!SETTINGS.get_boolean('isolate-monitors') ||
global.display.focus_window.get_monitor() === this.dtpPanel.monitor.index);
}
_setAppIconPadding() {
let padding = getIconPadding(this.dtpPanel.monitor.index);
let margin = SETTINGS.get_int('appicon-margin');
this.set_style('padding:' + (this.dtpPanel.checkIfVertical() ? margin + 'px 0' : '0 ' + margin + 'px;'));
this._iconContainer.set_style('padding: ' + padding + 'px;');
}
popupMenu() {
this._removeMenuTimeout();
this.fake_release();
if (!this._menu) {
this._menu = new TaskbarSecondaryMenu(this, this.dtpPanel.geom.position);
this._menu.setApp(this.app);
this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
if (!isPoppedUp)
this._onMenuPoppedDown();
else
this._previewMenu.close(true);
});
let id = Main.overview.connect('hiding', () => {
this._menu.close();
});
this.connect('destroy', () => {
Main.overview.disconnect(id);
});
// We want to keep the item hovered while the menu is up
this._menu.blockSourceEvents = true;
Main.uiGroup.add_actor(this._menu.actor);
this._menuManager.addMenu(this._menu);
}
this._menu.updateQuitText();
this.emit('menu-state-changed', true);
this.set_hover(true);
this._menu.open(BoxPointer.PopupAnimation.FULL);
this._menuManager.ignoreRelease();
this.emit('sync-tooltip');
return false;
}
_onFocusAppChanged(windowTracker) {
this._displayProperIndicator();
}
_onOverviewWindowDragEnd(windowTracker) {
this._timeoutsHandler.add([T4, 0, () => {
if (SETTINGS.get_boolean('isolate-workspaces'))
this._updateWindows()
this._displayProperIndicator()
}]);
}
_onSwitchWorkspace(windowTracker) {
if (this._isGroupApps) {
this._timeoutsHandler.add([T5, 0, () => this._displayProperIndicator()]);
} else {
this._displayProperIndicator();
}
}
_displayProperIndicator() {
let isFocused = this._isFocusedWindow();
let position = SETTINGS.get_string('dot-position');
let isHorizontalDots = position == DOT_POSITION.TOP || position == DOT_POSITION.BOTTOM;
this._setIconStyle(isFocused);
if(!this._isGroupApps) {
if (this.window && (SETTINGS.get_boolean('group-apps-underline-unfocused') || isFocused)) {
let align = Clutter.ActorAlign[position == DOT_POSITION.TOP || position == DOT_POSITION.LEFT ? 'START' : 'END'];
this._focusedDots.set_size(0, 0);
this._focusedDots[isHorizontalDots ? 'height' : 'width'] = this._getRunningIndicatorSize();
this._focusedDots.y_align = this._focusedDots.x_align = Clutter.ActorAlign.FILL;
this._focusedDots[(isHorizontalDots ? 'y' : 'x') + '_align'] = align;
this._focusedDots.background_color = this._getRunningIndicatorColor(isFocused);
this._focusedDots.show();
} else if (this._focusedDots.visible) {
this._focusedDots.hide();
}
} else {
let sizeProp = isHorizontalDots ? 'width' : 'height';
let newFocusedDotsSize = 0;
let newFocusedDotsOpacity = 0;
let newUnfocusedDotsSize = 0;
let newUnfocusedDotsOpacity = 0;
isFocused = this._checkIfFocusedApp() && this._checkIfMonitorHasFocus();
this._timeoutsHandler.add([T6, 0, () => {
if(isFocused)
this.add_style_class_name('focused');
else
this.remove_style_class_name('focused');
}]);
if(this._focusedIsWide) {
newFocusedDotsSize = (isFocused && this._nWindows > 0) ? this._containerSize : 0;
newFocusedDotsOpacity = 255;
} else {
newFocusedDotsSize = this._containerSize;
newFocusedDotsOpacity = (isFocused && this._nWindows > 0) ? 255 : 0;
}
if(this._unfocusedIsWide) {
newUnfocusedDotsSize = (!isFocused && this._nWindows > 0) ? this._containerSize : 0;
newUnfocusedDotsOpacity = 255;
} else {
newUnfocusedDotsSize = this._containerSize;
newUnfocusedDotsOpacity = (!isFocused && this._nWindows > 0) ? 255 : 0;
}
// Only animate if...
// animation is enabled in settings
// AND (going from a wide style to a narrow style indicator or vice-versa
// OR going from an open app to a closed app or vice versa)
let animate = SETTINGS.get_boolean('animate-app-switch') &&
((this._focusedIsWide != this._unfocusedIsWide) ||
(this._focusedDots[sizeProp] != newUnfocusedDotsSize || this._unfocusedDots[sizeProp] != newFocusedDotsSize))
let duration = animate ? Taskbar.DASH_ANIMATION_TIME : 0.001;
this._animateDotDisplay(this._focusedDots, newFocusedDotsSize, this._unfocusedDots, newUnfocusedDotsOpacity, sizeProp, duration);
this._animateDotDisplay(this._unfocusedDots, newUnfocusedDotsSize, this._focusedDots, newFocusedDotsOpacity, sizeProp, duration);
}
}
_animateDotDisplay(dots, newSize, otherDots, newOtherOpacity, sizeProp, duration) {
Utils.stopAnimations(dots)
let tweenOpts = {
time: duration,
transition: 'easeInOutCubic',
onComplete: () => {
if(newOtherOpacity > 0)
otherDots.opacity = newOtherOpacity;
}
};
if(newOtherOpacity == 0)
otherDots.opacity = newOtherOpacity;
tweenOpts[sizeProp] = newSize;
Utils.animate(dots, tweenOpts);
}
_isFocusedWindow() {
let focusedWindow = global.display.focus_window;
while (focusedWindow) {
if (focusedWindow == this.window) {
return true;
}
focusedWindow = focusedWindow.get_transient_for();
}
return false;
}
_isWideDotStyle(dotStyle) {
return dotStyle == DOT_STYLE.SEGMENTED ||
dotStyle == DOT_STYLE.CILIORA ||
dotStyle == DOT_STYLE.METRO ||
dotStyle == DOT_STYLE.SOLID;
}
_isThemeProvidingIndicator() {
// This is an attempt to determine if the theme is providing their own
// running indicator by way of a border image on the icon, for example in
// the theme Ciliora
return (this.icon.get_stage() &&
this.icon.get_theme_node().get_border_image());
}
activate(button, modifiers, handleAsGrouped) {
let event = Clutter.get_current_event();
modifiers = event ? event.get_state() : modifiers || 0;
// Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.)
modifiers = modifiers & (Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK);
let ctrlPressed = modifiers & Clutter.ModifierType.CONTROL_MASK
if (ctrlPressed) {
// CTRL-click or hotkey with ctrl
return this._launchNewInstance(true);
}
// We check what type of click we have and if the modifier SHIFT is
// being used. We then define what buttonAction should be for this
// event.
let buttonAction = 0;
let doubleClick;
if (button && button == 2 ) {
if (modifiers & Clutter.ModifierType.SHIFT_MASK)
buttonAction = SETTINGS.get_string('shift-middle-click-action');
else
buttonAction = SETTINGS.get_string('middle-click-action');
}
else if (button && button == 1) {
let now = global.get_current_time()
doubleClick = now - this.lastClick < DOUBLE_CLICK_DELAY_MS
this.lastClick = now
if (modifiers & Clutter.ModifierType.SHIFT_MASK)
buttonAction = SETTINGS.get_string('shift-click-action');
else
buttonAction = SETTINGS.get_string('click-action');
}
let closePreview = () => this._previewMenu.close(SETTINGS.get_boolean('window-preview-hide-immediate-click'));
let appCount = this.getAppIconInterestingWindows().length;
let previewedAppIcon = this._previewMenu.getCurrentAppIcon();
if (this.window || buttonAction != 'TOGGLE-SHOWPREVIEW')
closePreview()
// We check if the app is running, and that the # of windows is > 0 in
// case we use workspace isolation,
let appIsRunning = this.app.state == Shell.AppState.RUNNING && appCount > 0;
// We customize the action only when the application is already running
if (appIsRunning && !this.isLauncher) {
if (this.window && !handleAsGrouped) {
//ungrouped applications behaviors
switch (buttonAction) {
case 'RAISE': case 'CYCLE': case 'CYCLE-MIN': case 'MINIMIZE': case 'TOGGLE-SHOWPREVIEW': case 'TOGGLE-CYCLE':
if (!Main.overview._shown &&
(buttonAction == 'MINIMIZE' || buttonAction == 'TOGGLE-SHOWPREVIEW' || buttonAction == 'TOGGLE-CYCLE' || buttonAction == 'CYCLE-MIN') &&
(this._isFocusedWindow() || (buttonAction == 'MINIMIZE' && (button == 2 || modifiers & Clutter.ModifierType.SHIFT_MASK)))) {
this.window.minimize();
} else {
Main.activateWindow(this.window);
}
break;
case "LAUNCH":
this._launchNewInstance();
break;
case "QUIT":
this.window.delete(global.get_current_time());
break;
}
} else {
//grouped application behaviors
let monitor = this.dtpPanel.monitor;
let appHasFocus = this._checkIfFocusedApp() && this._checkIfMonitorHasFocus();
switch (buttonAction) {
case "RAISE":
activateAllWindows(this.app, monitor);
break;
case "LAUNCH":
this._launchNewInstance();
break;
case "MINIMIZE":
// In overview just activate the app, unless the acion is explicitely
// requested with a keyboard modifier
if (!Main.overview._shown || modifiers){
// If we have button=2 or a modifier, allow minimization even if
// the app is not focused
if (appHasFocus || button == 2 || modifiers & Clutter.ModifierType.SHIFT_MASK) {
// minimize all windows on double click and always in the case of primary click without
// additional modifiers
let all_windows = (button == 1 && ! modifiers) || doubleClick;
minimizeWindow(this.app, all_windows, monitor);
}
else
activateAllWindows(this.app, monitor);
}
else
this.app.activate();
break;
case "CYCLE":
if (!Main.overview._shown){
if (appHasFocus)
cycleThroughWindows(this.app, false, false, monitor);
else {
activateFirstWindow(this.app, monitor);
}
}
else
this.app.activate();
break;
case "CYCLE-MIN":
if (!Main.overview._shown){
if (appHasFocus || (recentlyClickedApp == this.app && recentlyClickedAppWindows[recentlyClickedAppIndex % recentlyClickedAppWindows.length] == "MINIMIZE"))
cycleThroughWindows(this.app, false, true, monitor);
else {
activateFirstWindow(this.app, monitor);
}
}
else
this.app.activate();
break;
case "TOGGLE-SHOWPREVIEW":
if (!Main.overview._shown) {
if (appCount == 1) {
closePreview()
if (appHasFocus)
minimizeWindow(this.app, false, monitor);
else
activateFirstWindow(this.app, monitor);
} else {
if (doubleClick) {
// minimize all windows if double clicked
closePreview()
minimizeWindow(this.app, true, monitor);
} else if (previewedAppIcon != this) {
this._previewMenu.open(this);
}
this.emit('sync-tooltip');
}
}
else
this.app.activate();
break;
case "TOGGLE-CYCLE":
if (!Main.overview._shown) {
if (appCount == 1) {
if (appHasFocus)
minimizeWindow(this.app, false, monitor);
else
activateFirstWindow(this.app, monitor);
} else {
cycleThroughWindows(this.app, false, false, monitor);
}
}
else
this.app.activate();
break;
case "QUIT":
closeAllWindows(this.app, monitor);
break;
}
}
}
else {
this._launchNewInstance();
}
global.display.emit('grab-op-begin', null, null);
Main.overview.hide();
}
_launchNewInstance(ctrlPressed) {
let maybeAnimate = () => SETTINGS.get_boolean('animate-window-launch') && this.animateLaunch()
if ((ctrlPressed || this.app.state == Shell.AppState.RUNNING) &&
this.app.can_open_new_window()) {
maybeAnimate();
this.app.open_new_window(-1);
} else {
let windows = this.window ? [this.window] : this.app.get_windows();
if (windows.length) {
Main.activateWindow(windows[0]);
} else {
maybeAnimate();
this.app.activate();
}
}
}
_updateWindows() {
let windows = [this.window];
if (!this.window) {
windows = this.getAppIconInterestingWindows();
this._nWindows = windows.length;
for (let i = 1; i <= MAX_INDICATORS; i++){
let className = 'running'+i;
if(i != this._nWindows)
this.remove_style_class_name(className);
else
this.add_style_class_name(className);
}
}
this._previewMenu.update(this, windows);
}
_getRunningIndicatorCount() {
return Math.min(this._nWindows, MAX_INDICATORS);
}
_getRunningIndicatorSize() {
return SETTINGS.get_int('dot-size') * Utils.getScaleFactor();
}
_getRunningIndicatorColor(isFocused) {
let color;
const fallbackColor = new Clutter.Color({ red: 82, green: 148, blue: 226, alpha: 255 });
if (SETTINGS.get_boolean('dot-color-dominant')) {
let dce = new Utils.DominantColorExtractor(this.app);
let palette = dce._getColorPalette();
if (palette) {
color = Clutter.color_from_string(palette.original)[1];
} else { // unable to determine color, fall back to theme
let themeNode = this._dot.get_theme_node();
color = themeNode.get_background_color();
// theme didn't provide one, use a default
if(color.alpha == 0) color = fallbackColor;
}
} else if(SETTINGS.get_boolean('dot-color-override')) {
let dotColorSettingPrefix = 'dot-color-';
if(!isFocused && SETTINGS.get_boolean('dot-color-unfocused-different'))
dotColorSettingPrefix = 'dot-color-unfocused-';
color = Clutter.color_from_string(SETTINGS.get_string(dotColorSettingPrefix + (this._getRunningIndicatorCount() || 1) ))[1];
} else {
// Re-use the style - background color, and border width and color -
// of the default dot
let themeNode = this._dot.get_theme_node();
color = themeNode.get_background_color();
// theme didn't provide one, use a default
if(color.alpha == 0) color = fallbackColor;
}
return color;
}
_getFocusHighlightColor() {
if (SETTINGS.get_boolean('focus-highlight-dominant')) {
let dce = new Utils.DominantColorExtractor(this.app);
let palette = dce._getColorPalette();
if (palette) return palette.original;
}
return SETTINGS.get_string('focus-highlight-color');
}
_drawRunningIndicator(area, type, isFocused) {
let n = this._getRunningIndicatorCount();
if (!n) {
return;
}
let position = SETTINGS.get_string('dot-position');
let isHorizontalDots = position == DOT_POSITION.TOP || position == DOT_POSITION.BOTTOM;
let bodyColor = this._getRunningIndicatorColor(isFocused);
let [areaWidth, areaHeight] = area.get_surface_size();
let cr = area.get_context();
let size = this._getRunningIndicatorSize();
let areaSize = areaWidth;
let startX = 0;
let startY = 0;
if (isHorizontalDots) {
if (position == DOT_POSITION.BOTTOM) {
startY = areaHeight - size;
}
} else {
areaSize = areaHeight;
if (position == DOT_POSITION.RIGHT) {
startX = areaWidth - size;
}
}
if (type == DOT_STYLE.SOLID || type == DOT_STYLE.METRO) {
if (type == DOT_STYLE.SOLID || n <= 1) {
cr.translate(startX, startY);
Clutter.cairo_set_source_color(cr, bodyColor);
cr.newSubPath();
cr.rectangle.apply(cr, [0, 0].concat(isHorizontalDots ? [areaSize, size] : [size, areaSize]));
cr.fill();
} else {
let blackenedLength = (1 / 48) * areaSize; // need to scale with the SVG for the stacked highlight
let darkenedLength = isFocused ? (2 / 48) * areaSize : (10 / 48) * areaSize;
let blackenedColor = bodyColor.shade(.3);
let darkenedColor = bodyColor.shade(.7);
let solidDarkLength = areaSize - darkenedLength;
let solidLength = solidDarkLength - blackenedLength;
cr.translate(startX, startY);
Clutter.cairo_set_source_color(cr, bodyColor);
cr.newSubPath();
cr.rectangle.apply(cr, [0, 0].concat(isHorizontalDots ? [solidLength, size] : [size, solidLength]));
cr.fill();
Clutter.cairo_set_source_color(cr, blackenedColor);
cr.newSubPath();
cr.rectangle.apply(cr, isHorizontalDots ? [solidLength, 0, 1, size] : [0, solidLength, size, 1]);
cr.fill();
Clutter.cairo_set_source_color(cr, darkenedColor);
cr.newSubPath();
cr.rectangle.apply(cr, isHorizontalDots ? [solidDarkLength, 0, darkenedLength, size] : [0, solidDarkLength, size, darkenedLength]);
cr.fill();
}
} else {
let spacing = Math.ceil(areaSize / 18); // separation between the indicators
let length;
let dist;
let indicatorSize;
let translate;
let preDraw = () => {};
let draw;
let drawDash = (i, dashLength) => {
dist = i * dashLength + i * spacing;
cr.rectangle.apply(cr, (isHorizontalDots ? [dist, 0, dashLength, size] : [0, dist, size, dashLength]));
};
switch (type) {
case DOT_STYLE.CILIORA:
spacing = size;
length = areaSize - (size * (n - 1)) - (spacing * (n - 1));
translate = () => cr.translate(startX, startY);
preDraw = () => {
cr.newSubPath();
cr.rectangle.apply(cr, [0, 0].concat(isHorizontalDots ? [length, size] : [size, length]));
};
draw = i => {
dist = length + (i * spacing) + ((i - 1) * size);
cr.rectangle.apply(cr, (isHorizontalDots ? [dist, 0] : [0, dist]).concat([size, size]));
};
break;
case DOT_STYLE.DOTS:
let radius = size / 2;
translate = () => {
indicatorSize = Math.floor((areaSize - n * size - (n - 1) * spacing) / 2);
cr.translate.apply(cr, isHorizontalDots ? [indicatorSize, startY] : [startX, indicatorSize]);
}
draw = i => {
dist = (2 * i + 1) * radius + i * spacing;
cr.arc.apply(cr, (isHorizontalDots ? [dist, radius] : [radius, dist]).concat([radius, 0, 2 * Math.PI]));
};
break;
case DOT_STYLE.SQUARES:
translate = () => {
indicatorSize = Math.floor((areaSize - n * size - (n - 1) * spacing) / 2);
cr.translate.apply(cr, isHorizontalDots ? [indicatorSize, startY] : [startX, indicatorSize]);
}
draw = i => {
dist = i * size + i * spacing;
cr.rectangle.apply(cr, (isHorizontalDots ? [dist, 0] : [0, dist]).concat([size, size]));
};
break;
case DOT_STYLE.DASHES:
length = Math.floor(areaSize / 4) - spacing;
translate = () => {
indicatorSize = Math.floor((areaSize - n * length - (n - 1) * spacing) / 2);
cr.translate.apply(cr, isHorizontalDots ? [indicatorSize, startY] : [startX, indicatorSize]);
}
draw = i => drawDash(i, length);
break;
case DOT_STYLE.SEGMENTED:
length = Math.ceil((areaSize - ((n - 1) * spacing)) / n);
translate = () => cr.translate(startX, startY);
draw = i => drawDash(i, length);
break;
}
translate();
Clutter.cairo_set_source_color(cr, bodyColor);
preDraw();
for (let i = 0; i < n; i++) {
cr.newSubPath();
draw(i);
}
cr.fill();
}
cr.$dispose();
}
_numberOverlay() {
// Add label for a Hot-Key visual aid
this._numberOverlayLabel = new St.Label({ style_class: 'badge' });
this._numberOverlayBin = new St.Bin({
child: this._numberOverlayLabel, y: 2
});
this._numberOverlayLabel.add_style_class_name('number-overlay');
this._numberOverlayOrder = -1;
this._numberOverlayBin.hide();
this._dtpIconContainer.add_child(this._numberOverlayBin);
}
updateHotkeyNumberOverlay() {
this.updateNumberOverlay(this._numberOverlayBin, true);
}
updateNumberOverlay(bin, fixedSize) {
// We apply an overall scale factor that might come from a HiDPI monitor.
// Clutter dimensions are in physical pixels, but CSS measures are in logical
// pixels, so make sure to consider the scale.
// Set the font size to something smaller than the whole icon so it is
// still visible. The border radius is large to make the shape circular
let [minWidth, natWidth] = this._dtpIconContainer.get_preferred_width(-1);
let font_size = Math.round(Math.max(12, 0.3 * natWidth) / Utils.getScaleFactor());
let size = Math.round(font_size * 1.3);
let label = bin.child;
let style = 'font-size: ' + font_size + 'px;' +
'border-radius: ' + this.icon.iconSize + 'px;' +
'height: ' + size +'px;';
if (fixedSize || label.get_text().length == 1) {
style += 'width: ' + size + 'px;';
} else {
style += 'padding: 0 2px;';
}
bin.x = 2;
label.set_style(style);
}
setNumberOverlay(number) {
this._numberOverlayOrder = number;
this._numberOverlayLabel.set_text(number.toString());
}
toggleNumberOverlay(activate) {
if (activate && this._numberOverlayOrder > -1)
this._numberOverlayBin.show();
else
this._numberOverlayBin.hide();
}
handleDragOver(source, actor, x, y, time) {
if (source == Main.xdndHandler) {
this._previewMenu.close(true);
}
return DND.DragMotionResult.CONTINUE;
}
getAppIconInterestingWindows(isolateMonitors) {
return getInterestingWindows(this.app, this.dtpPanel.monitor, isolateMonitors);
}
});
TaskbarAppIcon.prototype.scaleAndFade = TaskbarAppIcon.prototype.undoScaleAndFade = () => {};
export function minimizeWindow(app, param, monitor){
// Param true make all app windows minimize
let windows = getInterestingWindows(app, monitor);
let current_workspace = Utils.DisplayWrapper.getWorkspaceManager().get_active_workspace();
for (let i = 0; i < windows.length; i++) {
let w = windows[i];
if (w.get_workspace() == current_workspace && w.showing_on_its_workspace()){
w.minimize();
// Just minimize one window. By specification it should be the
// focused window on the current workspace.
if(!param)
break;
}
}
}
/*
* By default only non minimized windows are activated.
* This activates all windows in the current workspace.
*/
export function activateAllWindows(app, monitor){
// First activate first window so workspace is switched if needed,
// then activate all other app windows in the current workspace.
let windows = getInterestingWindows(app, monitor);
let w = windows[0];
Main.activateWindow(w);
let activeWorkspace = Utils.DisplayWrapper.getWorkspaceManager().get_active_workspace_index();
if (windows.length <= 0)
return;
for (let i = windows.length - 1; i >= 0; i--){
if (windows[i].get_workspace().index() == activeWorkspace){
Main.activateWindow(windows[i]);
}
}
}
export function activateFirstWindow(app, monitor){
let windows = getInterestingWindows(app, monitor);
Main.activateWindow(windows[0]);
}
export function cycleThroughWindows(app, reversed, shouldMinimize, monitor) {
// Store for a little amount of time last clicked app and its windows
// since the order changes upon window interaction
let MEMORY_TIME=3000;
let app_windows = getInterestingWindows(app, monitor);
if(shouldMinimize)
app_windows.push("MINIMIZE");
if (recentlyClickedAppLoopId > 0)
GLib.Source.remove(recentlyClickedAppLoopId);
recentlyClickedAppLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
MEMORY_TIME, resetRecentlyClickedApp);
// If there isn't already a list of windows for the current app,
// or the stored list is outdated, use the current windows list.
if (!recentlyClickedApp ||
recentlyClickedApp.get_id() != app.get_id() ||
recentlyClickedAppWindows.length != app_windows.length ||
recentlyClickedAppMonitorIndex != monitor.index) {
recentlyClickedApp = app;
recentlyClickedAppWindows = app_windows;
recentlyClickedAppIndex = 0;
recentlyClickedAppMonitorIndex = monitor.index;
}
if (reversed) {
recentlyClickedAppIndex--;
if (recentlyClickedAppIndex < 0) recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1;
} else {
recentlyClickedAppIndex++;
}
let index = recentlyClickedAppIndex % recentlyClickedAppWindows.length;
if(recentlyClickedAppWindows[index] === "MINIMIZE")
minimizeWindow(app, true, monitor);
else
Main.activateWindow(recentlyClickedAppWindows[index]);
}
export function resetRecentlyClickedApp() {
if (recentlyClickedAppLoopId > 0)
GLib.Source.remove(recentlyClickedAppLoopId);
recentlyClickedAppLoopId=0;
recentlyClickedApp =null;
recentlyClickedAppWindows = null;
recentlyClickedAppIndex = 0;
recentlyClickedAppMonitorIndex = null;
return false;
}
export function closeAllWindows(app, monitor) {
let windows = getInterestingWindows(app, monitor);
for (let i = 0; i < windows.length; i++)
windows[i].delete(global.get_current_time());
}
// Filter out unnecessary windows, for instance
// nautilus desktop window.
export function getInterestingWindows(app, monitor, isolateMonitors) {
let windows = (
app ?
app.get_windows() :
global.get_window_actors().map(wa => wa.get_meta_window())
).filter(w => !w.skip_taskbar);
// When using workspace or monitor isolation, we filter out windows
// that are not in the current workspace or on the same monitor as the appicon
if (SETTINGS.get_boolean('isolate-workspaces'))
windows = windows.filter(function(w) {
return w.get_workspace() &&
w.get_workspace() == Utils.getCurrentWorkspace();
});
if (monitor && SETTINGS.get_boolean('multi-monitors') && (isolateMonitors || SETTINGS.get_boolean('isolate-monitors'))) {
windows = windows.filter(function(w) {
return w.get_monitor() == monitor.index;
});
}
return windows;
}
export function cssHexTocssRgba(cssHex, opacity) {
let bigint = parseInt(cssHex.slice(1), 16);
let r = (bigint >> 16) & 255;
let g = (bigint >> 8) & 255;
let b = bigint & 255;
return 'rgba(' + [r, g, b].join(',') + ',' + opacity + ')';
}
export function getIconPadding(monitorIndex) {
let panelSize = PanelSettings.getPanelSize(SETTINGS, monitorIndex);
let padding = SETTINGS.get_int('appicon-padding');
let availSize = panelSize - Taskbar.MIN_ICON_SIZE - panelSize % 2;
if (padding * 2 > availSize) {
padding = availSize * .5;
}
return padding;
}
/**
* Extend AppMenu (AppIconMenu for pre gnome 41)
*
* - hide 'Show Details' according to setting
* - show windows header only if show-window-previews is disabled
* - Add close windows option based on quitfromdash extension
* (https://github.com/deuill/shell-extension-quitfromdash)
*/
export class TaskbarSecondaryMenu extends AppMenu.AppMenu {
constructor(source, side) {
super(source, side);
// constructor parameter does nos work for some reason
this._enableFavorites = true;
this._showSingleWindows = true;
// Remove "Show Details" menu item
if(!SETTINGS.get_boolean('secondarymenu-contains-showdetails')) {
let existingMenuItems = this._getMenuItems();
for (let i = 0; i < existingMenuItems.length; i++) {
let item = existingMenuItems[i];
if (item !== undefined && item.label !== undefined) {
if (item.label.text == "Show Details") {
this.box.remove_child(item.actor);
}
}
}
}
// replace quit item
delete this._quitItem;
this._quitItem = this.addAction(_('Quit'), () => this._quitFromTaskbar());
}
updateQuitText() {
let count = this.sourceActor.window ? 1 :
getInterestingWindows(this._app, this.sourceActor.dtpPanel.monitor).length;
if ( count > 0) {
let quitFromTaskbarMenuText = "";
if (count == 1)
quitFromTaskbarMenuText = _("Quit");
else
quitFromTaskbarMenuText = _("Quit") + ' ' + count + ' ' + _("Windows");
this._quitItem.label.set_text(quitFromTaskbarMenuText);
}
}
_quitFromTaskbar() {
let time = global.get_current_time()
let windows =
this.sourceActor.window ? // ungrouped applications
[this.sourceActor.window] :
getInterestingWindows(this._app, this.sourceActor.dtpPanel.monitor)
if (windows.length == this._app.get_windows().length)
this._app.request_quit()
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
windows.forEach((w) => !!w.get_compositor_private() && w.delete(time++));
return GLib.SOURCE_REMOVE;
});
}
}
/**
* This function is used for extendDashItemContainer
*/
export function ItemShowLabel() {
if (!this._labelText)
return;
this.label.set_text(this._labelText);
this.label.opacity = 0;
this.label.show();
let [stageX, stageY] = this.get_transformed_position();
let node = this.label.get_theme_node();
let itemWidth = this.allocation.x2 - this.allocation.x1;
let itemHeight = this.allocation.y2 - this.allocation.y1;
let labelWidth = this.label.get_width();
let labelHeight = this.label.get_height();
let position = this._dtpPanel.getPosition();
let labelOffset = node.get_length('-x-offset');
// From TaskbarItemContainer
if (this._getIconAnimationOffset)
labelOffset += this._getIconAnimationOffset();
let xOffset = Math.floor((itemWidth - labelWidth) / 2);
let x = stageX + xOffset
let y = stageY + (itemHeight - labelHeight) * .5;
switch(position) {
case St.Side.TOP:
y = stageY + labelOffset + itemHeight;
break;
case St.Side.BOTTOM:
y = stageY - labelHeight - labelOffset;
break;
case St.Side.LEFT:
x = stageX + labelOffset + itemWidth;
break;
case St.Side.RIGHT:
x = stageX - labelWidth - labelOffset;
break;
}
// keep the label inside the screen border
// Only needed for the x coordinate.
// Leave a few pixel gap
let gap = LABEL_GAP;
let monitor = Main.layoutManager.findMonitorForActor(this);
if ( x - monitor.x < gap)
x += monitor.x - x + labelOffset;
else if ( x + labelWidth > monitor.x + monitor.width - gap)
x -= x + labelWidth -( monitor.x + monitor.width) + gap;
this.label.set_position(Math.round(x), Math.round(y));
let duration = Dash.DASH_ITEM_LABEL_SHOW_TIME;
if (duration > 1) {
duration /= 1000;
}
Utils.animate(this.label, {
opacity: 255,
time: duration,
transition: 'easeOutQuad',
});
}
/**
* A wrapper class around the ShowAppsIcon class.
*
* - Pass settings to the constructor
* - set label position based on dash orientation (Note, I am reusing most machinery of the appIcon class)
* - implement a popupMenu based on the AppIcon code (Note, I am reusing most machinery of the appIcon class)
*
* I can't subclass the original object because of this: https://bugzilla.gnome.org/show_bug.cgi?id=688973.
* thus use this pattern where the real showAppsIcon object is encaptulated, and a reference to it will be properly wired upon
* use of this class in place of the original showAppsButton.
*
*/
export const ShowAppsIconWrapper = class extends EventEmitter {
constructor(dtpPanel) {
super();
this.realShowAppsIcon = new Dash.ShowAppsIcon();
/* the variable equivalent to toggleButton has a different name in the appIcon class
(actor): duplicate reference to easily reuse appIcon methods */
this.actor = this.realShowAppsIcon.toggleButton;
this.realShowAppsIcon.show(false);
// Re-use appIcon methods
this._removeMenuTimeout = AppDisplay.AppIcon.prototype._removeMenuTimeout;
this._setPopupTimeout = AppDisplay.AppIcon.prototype._setPopupTimeout;
this._onKeyboardPopupMenu = AppDisplay.AppIcon.prototype._onKeyboardPopupMenu;
// No action on clicked (showing of the appsview is controlled elsewhere)
this._onClicked = (actor, button) => this._removeMenuTimeout();
this.actor.connect('leave-event', this._onLeaveEvent.bind(this));
this.actor.connect('button-press-event', this._onButtonPress.bind(this));
this.actor.connect('touch-event', this._onTouchEvent.bind(this));
this.actor.connect('clicked', this._onClicked.bind(this));
this.actor.connect('popup-menu', this._onKeyboardPopupMenu.bind(this));
this._menu = null;
this._menuManager = new PopupMenu.PopupMenuManager(this.actor);
this._menuTimeoutId = 0;
this.realShowAppsIcon._dtpPanel = dtpPanel;
Taskbar.extendDashItemContainer(this.realShowAppsIcon);
let customIconPath = SETTINGS.get_string('show-apps-icon-file');
this.realShowAppsIcon.icon.createIcon = function(size) {
this._iconActor = new St.Icon({ icon_name: 'view-app-grid-symbolic',
icon_size: size,
style_class: 'show-apps-icon',
track_hover: true });
if (customIconPath) {
this._iconActor.gicon = new Gio.FileIcon({ file: Gio.File.new_for_path(customIconPath) });
}
return this._iconActor;
};
this._changedShowAppsIconId = SETTINGS.connect('changed::show-apps-icon-file', () => {
customIconPath = SETTINGS.get_string('show-apps-icon-file');
this.realShowAppsIcon.icon._createIconTexture(this.realShowAppsIcon.icon.iconSize);
});
this._changedAppIconPaddingId = SETTINGS.connect('changed::appicon-padding', () => this.setShowAppsPadding());
this._changedAppIconSidePaddingId = SETTINGS.connect('changed::show-apps-icon-side-padding', () => this.setShowAppsPadding());
this.setShowAppsPadding();
}
_onButtonPress(_actor, event) {
let button = event.get_button();
if (button == 1) {
this._setPopupTimeout();
} else if (button == 3) {
this.popupMenu();
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
}
_onLeaveEvent(_actor, _event) {
this.actor.fake_release();
this._removeMenuTimeout();
}
_onTouchEvent(actor, event) {
if (event.type() == Clutter.EventType.TOUCH_BEGIN)
this._setPopupTimeout();
return Clutter.EVENT_PROPAGATE;
}
_onMenuPoppedDown() {
this._menu.sourceActor = this.actor;
this.actor.sync_hover();
this.emit('menu-state-changed', false);
}
setShowAppsPadding() {
let padding = getIconPadding(this.realShowAppsIcon._dtpPanel.monitor.index);
let sidePadding = SETTINGS.get_int('show-apps-icon-side-padding');
let isVertical = this.realShowAppsIcon._dtpPanel.checkIfVertical();
this.actor.set_style('padding:' + (padding + (isVertical ? sidePadding : 0)) + 'px ' + (padding + (isVertical ? 0 : sidePadding)) + 'px;');
}
createMenu() {
if (!this._menu) {
this._menu = new MyShowAppsIconMenu(this.realShowAppsIcon, this.realShowAppsIcon._dtpPanel);
this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
if (!isPoppedUp)
this._onMenuPoppedDown();
});
let id = Main.overview.connect('hiding', () => {
this._menu.close();
});
this._menu.actor.connect('destroy', () => {
Main.overview.disconnect(id);
});
// We want to keep the item hovered while the menu is up
this._menu.blockSourceEvents = true;
Main.uiGroup.add_actor(this._menu.actor);
this._menuManager.addMenu(this._menu);
}
}
popupMenu(sourceActor = null) {
this._removeMenuTimeout();
this.actor.fake_release();
this.createMenu();
this._menu.updateItems(sourceActor == null ? this.realShowAppsIcon : sourceActor);
this.actor.set_hover(true);
this._menu.open(BoxPointer.PopupAnimation.FULL);
this._menuManager.ignoreRelease();
this.emit('sync-tooltip');
return false;
}
shouldShowTooltip() {
return SETTINGS.get_boolean('show-tooltip') &&
(this.actor.hover && (!this._menu || !this._menu.isOpen));
}
destroy() {
SETTINGS.disconnect(this._changedShowAppsIconId);
SETTINGS.disconnect(this._changedAppIconSidePaddingId);
SETTINGS.disconnect(this._changedAppIconPaddingId);
this.realShowAppsIcon.destroy();
}
};
/**
* A menu for the showAppsIcon
*/
export const MyShowAppsIconMenu = class extends PopupMenu.PopupMenu {
constructor(actor, dtpPanel) {
super(actor, 0, dtpPanel.getPosition());
this._dtpPanel = dtpPanel;
this.updateItems(actor);
}
updateItems(sourceActor) {
this.sourceActor = sourceActor;
this.removeAll();
if (this.sourceActor != Main.layoutManager.dummyCursor) {
this._appendItem({
title: _('Power options'),
cmd: ['gnome-control-center', 'power']
});
this._appendItem({
title: _('Event logs'),
cmd: ['gnome-logs']
});
this._appendItem({
title: _('System'),
cmd: ['gnome-control-center', 'info-overview']
});
this._appendItem({
title: _('Device Management'),
cmd: ['gnome-control-center', 'display']
});
this._appendItem({
title: _('Disk Management'),
cmd: ['gnome-disks']
});
this._appendList(
SETTINGS.get_strv('show-apps-button-context-menu-commands'),
SETTINGS.get_strv('show-apps-button-context-menu-titles')
)
this._appendSeparator();
}
this._appendItem({
title: _('Terminal'),
cmd: ['gnome-terminal']
});
this._appendItem({
title: _('System monitor'),
cmd: ['gnome-system-monitor']
});
this._appendItem({
title: _('Files'),
cmd: ['nautilus']
});
this._appendItem({
title: _('Extensions'),
cmd: ['gnome-shell-extension-prefs']
});
this._appendItem({
title: _('Settings'),
cmd: ['gnome-control-center']
});
this._appendList(
SETTINGS.get_strv('panel-context-menu-commands'),
SETTINGS.get_strv('panel-context-menu-titles')
)
this._appendSeparator();
let lockTaskbarMenuItem = this._appendMenuItem(SETTINGS.get_boolean('taskbar-locked') ? _('Unlock taskbar') : _('Lock taskbar'));
lockTaskbarMenuItem.connect('activate', () => {
SETTINGS.set_boolean('taskbar-locked', !SETTINGS.get_boolean('taskbar-locked'));
});
let settingsMenuItem = this._appendMenuItem(_('Dash to Panel Settings'));
settingsMenuItem.connect('activate', () => DTP_EXTENSION.openPreferences())
if(this.sourceActor == Main.layoutManager.dummyCursor) {
this._appendSeparator();
let item = this._appendMenuItem(this._dtpPanel._restoreWindowList ? _('Restore Windows') : _('Show Desktop'));
item.connect('activate', this._dtpPanel._onShowDesktopButtonPress.bind(this._dtpPanel));
}
}
// Only add menu entries for commands that exist in path
_appendItem(info) {
if (GLib.find_program_in_path(info.cmd[0])) {
let item = this._appendMenuItem(_(info.title));
item.connect('activate', function() {
print("activated: " + info.title);
Util.spawn(info.cmd);
});
return item;
}
return null;
}
_appendList(commandList, titleList) {
if (commandList.length != titleList.length) {
return;
}
for (let entry = 0; entry < commandList.length; entry++) {
_appendItem({
title: titleList[entry],
cmd: commandList[entry].split(' ')
});
}
}
_appendSeparator() {
let separator = new PopupMenu.PopupSeparatorMenuItem();
this.addMenuItem(separator);
}
_appendMenuItem(labelText) {
// FIXME: app-well-menu-item style
let item = new PopupMenu.PopupMenuItem(labelText);
this.addMenuItem(item);
return item;
}
};
export const getIconContainerStyle = function(isVertical) {
let style = 'padding: ';
if (SETTINGS.get_boolean('group-apps')) {
style += (isVertical ? '0;' : '0 ' + DEFAULT_PADDING_SIZE + 'px;');
} else {
style += (isVertical ? '' : '0 ') + DEFAULT_PADDING_SIZE + 'px;';
}
return style;
}