/* * 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 . * * * 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, TERMINALSETTINGS, EXTENSION_PATH, } from './extension.js' import { gettext as _, ngettext, } 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 APPICON_STYLE = { NORMAL: 'NORMAL', SYMBOLIC: 'SYMBOLIC', GRAYSCALE: 'GRAYSCALE', } 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 this._appicon_normalstyle = '' this._appicon_hoverstyle = '' this._appicon_pressedstyle = '' 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({ style_class: 'dtp-dots-container', layout_manager: new Clutter.BinLayout(), }) this._dtpIconContainer = new St.Widget({ layout_manager: new Clutter.BinLayout(), style: getIconContainerStyle(panel.checkIfVertical()), }) this.remove_child(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._onAppIconHoverHighlightChanged() this._setAppIconPadding() this._setAppIconStyle() 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 this._fullscreenId = Utils.DisplayWrapper.getScreen().connect( 'in-fullscreen-changed', () => { if ( global.display.focus_window?.get_monitor() == this.dtpPanel.monitor.index && !this.dtpPanel.monitor.inFullscreen ) { this._resetDots(true) this._displayProperIndicator() } }, ) } 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._hoverChangeId2 = this.connect('notify::hover', () => this._onAppIconHoverChanged_GtkWorkaround(), ) this._pressedChangedId = this.connect('notify::pressed', () => this._onAppIconPressedChanged_GtkWorkaround(), ) this._dtpSettingsSignalIds = [ SETTINGS.connect( 'changed::animate-appicon-hover', this._onAnimateAppiconHoverChanged.bind(this), ), SETTINGS.connect( 'changed::animate-appicon-hover', this._onAppIconHoverHighlightChanged.bind(this), ), SETTINGS.connect( 'changed::highlight-appicon-hover', this._onAppIconHoverHighlightChanged.bind(this), ), SETTINGS.connect( 'changed::highlight-appicon-hover-background-color', this._onAppIconHoverHighlightChanged.bind(this), ), SETTINGS.connect( 'changed::highlight-appicon-pressed-background-color', this._onAppIconHoverHighlightChanged.bind(this), ), SETTINGS.connect( 'changed::highlight-appicon-hover-border-radius', this._onAppIconHoverHighlightChanged.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._dtpSettingsSignalIds = this._dtpSettingsSignalIds.concat([ SETTINGS.connect('changed::highlight-appicon-hover-border-radius', () => this._setIconStyle(this._isFocusedWindow()), ), ]) 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._dtpIconContainer, // which contains the icon but no highlighting elements // using this.actor directly would break DnD style. let cloneSource = this._dtpIconContainer let clone = new Clutter.Clone({ source: cloneSource, x: this.child.x, y: this.child.y, width: cloneSource.width, height: cloneSource.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._fullscreenId) Utils.DisplayWrapper.getScreen().disconnect(this._fullscreenId) 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._hoverChangeId2) { this.disconnect(this._hoverChangeId2) } if (this._pressedChangedId) { this.disconnect(this._pressedChangedId) } 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( 'child-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) } } } _onAppIconHoverHighlightChanged() { const background_color = SETTINGS.get_string( 'highlight-appicon-hover-background-color', ) const pressed_color = SETTINGS.get_string( 'highlight-appicon-pressed-background-color', ) const border_radius = SETTINGS.get_int( 'highlight-appicon-hover-border-radius', ) // Some trickery needed to get the effect const br = `border-radius: ${border_radius}px;` this._appicon_normalstyle = br this._container.set_style(this._appicon_normalstyle) this._appicon_hoverstyle = `background-color: ${background_color}; ${br}` this._appicon_pressedstyle = `background-color: ${pressed_color}; ${br}` if (SETTINGS.get_boolean('highlight-appicon-hover')) { this._container.remove_style_class_name('no-highlight') } else { this._container.add_style_class_name('no-highlight') this._appicon_normalstyle = '' this._appicon_hoverstyle = '' this._appicon_pressedstyle = '' } } _onAppIconHoverChanged_GtkWorkaround() { if (this.hover && this._appicon_hoverstyle) { this._container.set_style(this._appicon_hoverstyle) } else if (this._appicon_normalstyle) { this._container.set_style(this._appicon_normalstyle) } else { this._container.set_style('') } } _onAppIconPressedChanged_GtkWorkaround() { if (this.pressed && this._appicon_pressedstyle) { this._container.set_style(this._appicon_pressedstyle) } else if (this.hover && this._appicon_hoverstyle) { this._container.set_style(this._appicon_hoverstyle) } else if (this._appicon_normalstyle) { this._container.set_style(this._appicon_normalstyle) } else { this._container.set_style('') } } _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(ignoreSizeReset) { 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) if (!ignoreSizeReset) { ;[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, ) + ';' inlineStyle += this._appicon_normalstyle } 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() { const padding = getIconPadding(this.dtpPanel.monitor.index) const margin = SETTINGS.get_int('appicon-margin') const margin_todesktop = SETTINGS.get_int('appicon-margin-todesktop') const margin_toscreenborder = SETTINGS.get_int( 'appicon-margin-toscreenborder', ) let margin_style = '' const panelPosition = this.dtpPanel.getPosition() if (panelPosition == St.Side.TOP) { margin_style = `${margin_toscreenborder}px ${margin}px ${margin_todesktop}px ${margin}px` } else if (panelPosition == St.Side.RIGHT) { margin_style = `${margin}px ${margin_toscreenborder}px ${margin}px ${margin_todesktop}px` } else if (panelPosition == St.Side.LEFT) { margin_style = `${margin}px ${margin_todesktop}px ${margin}px ${margin_toscreenborder}px` } else { margin_style = `${margin_todesktop}px ${margin}px ${margin_toscreenborder}px ${margin}px` } this.set_style(`padding: ${margin_style};`) this._iconContainer.set_style('padding: ' + padding + 'px;') } _setAppIconStyle() { let appIconStyle = SETTINGS.get_string('appicon-style') if (appIconStyle === APPICON_STYLE.SYMBOLIC) { this.add_style_class_name('symbolic-icon-style') } else if (appIconStyle === APPICON_STYLE.GRAYSCALE) { this._iconContainer.add_effect_with_name( 'desaturate', new Clutter.DesaturateEffect({ factor: 1 }), ) } } 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_child(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') } // fixed issue #1676 by checking for button 0 or 1 to also handle touchscreen // input, probably not the proper fix as i'm not aware button 0 should exist // but from using this fix for months it seems to not create any issues else if (button === 0 || 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 Utils.ColorUtils.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 = Utils.ColorUtils.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 = Utils.ColorUtils.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) cr.setSourceColor(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 = new Utils.ColorUtils.Color({ red: bodyColor.red * 0.3, green: bodyColor.green * 0.3, blue: bodyColor.blue * 0.3, alpha: bodyColor.alpha, }) let darkenedColor = new Utils.ColorUtils.Color({ red: bodyColor.red * 0.7, green: bodyColor.green * 0.7, blue: bodyColor.blue * 0.7, alpha: bodyColor.alpha, }) let solidDarkLength = areaSize - darkenedLength let solidLength = solidDarkLength - blackenedLength cr.translate(startX, startY) cr.setSourceColor(bodyColor) cr.newSubPath() cr.rectangle.apply( cr, [0, 0].concat( isHorizontalDots ? [solidLength, size] : [size, solidLength], ), ) cr.fill() cr.setSourceColor(blackenedColor) cr.newSubPath() cr.rectangle.apply( cr, isHorizontalDots ? [solidLength, 0, 1, size] : [0, solidLength, size, 1], ) cr.fill() cr.setSourceColor(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() cr.setSourceColor(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 * 0.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 = ngettext( 'Quit %d Window', 'Quit %d Windows', count, ).format(count) 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 }) } setApp(app) { super.setApp(app) this._detailsItem.visible = !app.hideDetails } } /** * 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) * 0.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_child(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: [TERMINALSETTINGS.get_string('exec')], }) this._appendItem({ title: _('System monitor'), cmd: ['gnome-system-monitor'], }) this._appendItem({ title: _('Files'), cmd: ['nautilus'], }) this._appendItem({ title: _('Extensions'), cmd: ['gnome-extensions-app'], }) 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++) { this._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 }