/* * This file is part of the Zorin Taskbar extension for Zorin OS. * * 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 Dash to Panel extension * 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 { Hold } from './intellihide.js' import * as Utils from './utils.js' import * as PanelSettings from './panelSettings.js'; import * as Taskbar from './taskbar.js' import { DTP_EXTENSION, SETTINGS, DESKTOPSETTINGS, EXTENSION_PATH, tracker, } 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' const SCROLL_ICON_DELAY = 400 const DOT_SIZE = 3 //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 = 0 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 /** * 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._signalsHandler = new Utils.GlobalSignalsHandler() 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 () { 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.geom.vertical), }) this.remove_child(this._iconContainer) this.icon._iconBin.set_pivot_point(0.5, 0.5) this._dtpIconContainer.add_child(this._iconContainer) if (appInfo.window) { let box = Utils.createBoxLayout() this._windowTitle = new St.Label({ y_align: Clutter.ActorAlign.CENTER, x_align: Clutter.ActorAlign.START, style_class: 'overview-label', }) this._updateWindowTitle() 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.geom.vertical) { this.set_width(panel.geom.innerSize) } // 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._setAppIconPadding() this._showDots() this._numberOverlay() this._signalsHandler.add( [ this, 'notify::mapped', () => this.mapped && !this.dtpPanel.intellihide.enabled ? this._handleNotifications() : null, ], [ Utils.getStageTheme(), 'changed', this._updateWindowTitleStyle.bind(this), ], [ global.display, 'notify::focus-window', this._onFocusAppChanged.bind(this), ], [this.app, 'windows-changed', this.onWindowsChanged.bind(this)], ) if (!this.window) { if (SETTINGS.get_boolean('isolate-monitors')) { this._signalsHandler.add([ Utils.DisplayWrapper.getScreen(), ['window-entered-monitor', 'window-left-monitor'], this.onWindowEnteredOrLeft.bind(this), ]) } this._signalsHandler.add([ Utils.DisplayWrapper.getScreen(), '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._signalsHandler.add( [this.window, 'notify::title', this._updateWindowTitle.bind(this)], [ this.window, 'notify::minimized', this._updateWindowTitleStyle.bind(this), ], ) } this._signalsHandler.add( [this, 'scroll-event', this._onMouseScroll.bind(this)], [ Main.overview, 'window-drag-end', this._onOverviewWindowDragEnd.bind(this), ], [ global.window_manager, 'switch-workspace', this._onSwitchWorkspace.bind(this), ], [ this, 'notify::hover', () => { this._onAppIconHoverChanged() this._onAppIconHoverChanged_GtkWorkaround() }, ], [ this, 'notify::pressed', this._onAppIconPressedChanged_GtkWorkaround.bind(this), ], [ this.dtpPanel.panelManager.notificationsMonitor, `update-${this.app.id}`, this._handleNotifications.bind(this), ], [ SETTINGS, 'changed::progress-show-count', this._handleNotifications.bind(this), ], [ SETTINGS, [ 'changed::panel-positions', 'changed::dot-style-focused', 'changed::dot-style-unfocused', ], this._settingsChangeRefresh.bind(this), ], [ SETTINGS, [ 'changed::group-apps-label-max-width', 'changed::group-apps-use-fixed-width', ], this._updateWindowTitleStyle.bind(this), ], ) } getDragActor() { return this.app.create_icon_texture(this.dtpPanel.taskbar.iconSize) } 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() if (this._updateIconIdleId) { GLib.source_remove(this._updateIconIdleId) this._updateIconIdleId = 0 } this._timeoutsHandler.destroy() this._signalsHandler.destroy() this._previewMenu.close(true) } 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 || this._updateIconIdleId) return this._updateIconIdleId = GLib.idle_add(GLib.PRIORITY_LOW, () => { 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) }) this._updateIconIdleId = 0 return GLib.SOURCE_REMOVE }) } _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 === 'NOTHING' || (!this.window && !this._nWindows) ) { return } let direction = Utils.getMouseScrollDirection(event) if (direction && !this._timeoutsHandler.getId(T2)) { this._timeoutsHandler.add([ T2, 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 = PanelSettings.getPanelPosition(SETTINGS, this.dtpPanel.monitor.index) 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 scaleFactor = Utils.getScaleFactor() let maxLabelWidth = SETTINGS.get_int('group-apps-label-max-width') * scaleFactor let variableWidth = !useFixedWidth || this.dtpPanel.geom.vertical || 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( useFixedWidth ? '' : 'max-width: ' + maxLabelWidth + 'px;' ) } } _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 (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) const margin = Taskbar.APPICON_MARGIN let vertical = this.dtpPanel.geom.vertical this.set_style( `padding: ${vertical ? margin : 0}px ${vertical ? 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._signalsHandler.add( [ this._menu, 'open-state-changed', (menu, isPoppedUp) => { if (!isPoppedUp) this._onMenuPoppedDown() else this._previewMenu.close(true) }, ], [Main.overview, 'hiding', () => this._menu.close()], ) // 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.updateQuitItems() 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() { this._displayProperIndicator() } _onOverviewWindowDragEnd() { this._timeoutsHandler.add([ T4, 0, () => { if (SETTINGS.get_boolean('isolate-workspaces')) this._updateWindows() this._displayProperIndicator() }, ]) } _onSwitchWorkspace() { if (this._isGroupApps) { this._timeoutsHandler.add([T5, 0, () => this._displayProperIndicator()]) } else { this._displayProperIndicator() } } _displayProperIndicator() { let isFocused = this._isFocusedWindow() let position = PanelSettings.getPanelPosition(SETTINGS, this.dtpPanel.monitor.index) let isHorizontalDots = position == DOT_POSITION.TOP || position == DOT_POSITION.BOTTOM this._setIconStyle(isFocused) if (!this._isGroupApps) { if ( this.window && 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... // 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 = 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(true) 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 'LAUNCH': this._launchNewInstance() break case 'QUIT': this.window.delete(global.get_current_time()) break default: if ( !Main.overview._shown && (buttonAction == 'MINIMIZE' || buttonAction == 'TOGGLE-SHOWPREVIEW' || buttonAction == 'TOGGLE-CYCLE' || buttonAction == 'TOGGLE-SPREAD' || buttonAction == 'CYCLE-MIN') && (this._isFocusedWindow() || (buttonAction == 'MINIMIZE' && (button == 2 || modifiers & Clutter.ModifierType.SHIFT_MASK))) ) { this.window.minimize() } else { Main.activateWindow(this.window) } } } 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 case 'TOGGLE-SPREAD': if (appCount == 1) { if (appHasFocus && !Main.overview._shown) minimizeWindow(this.app, false, monitor) else activateFirstWindow(this.app, monitor) } else // return so the overview stays open if it already is return this.dtpPanel.panelManager.showFocusedAppInOverview( this.app, ) } } } else { this._launchNewInstance() } global.display.emit('grab-op-begin', null, null) Main.overview.hide() } _launchNewInstance(ctrlPressed) { if ( (ctrlPressed || this.app.state == Shell.AppState.RUNNING) && this.app.can_open_new_window() ) { this.animateLaunch() 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 { this.animateLaunch() 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 DOT_SIZE * Utils.getScaleFactor() } _getRunningIndicatorColor(isFocused) { let color const fallbackColor = new Utils.ColorUtils.Color({ red: 82, green: 148, blue: 226, alpha: 255, }) // 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 } _drawRunningIndicator(area, type, isFocused) { let n = this._getRunningIndicatorCount() if (!n) { return } let position = PanelSettings.getPanelPosition(SETTINGS, this.dtpPanel.monitor.index) 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 radius 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) { // Using the code from Ciliora Round style in pull request https://github.com/home-sweet-gnome/dash-to-panel/pull/1720 case DOT_STYLE.CILIORA: spacing = size radius = size / 2 length = areaSize - size * (n - 1) - spacing * (n - 1) translate = () => cr.translate(startX, startY) preDraw = () => { cr.newSubPath() cr.arc.apply( cr, (isHorizontalDots ? [radius, radius] : [radius, radius]).concat([radius, 0, 2 * Math.PI]) ) cr.arc.apply( cr, (isHorizontalDots ? [length - radius, radius] : [radius, length - radius]).concat([radius, 0, 2 * Math.PI]) ) cr.rectangle.apply( cr, (isHorizontalDots ? [radius, 0, length - size, size] : [0, radius, size, length - size]) ) } draw = (i) => { dist = length + i * spacing + (i - 1) * size + radius cr.arc.apply( cr, (isHorizontalDots ? [dist, radius] : [radius, dist]).concat([radius, 0, 2 * Math.PI]), ) } break case DOT_STYLE.DOTS: { 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() } _handleNotifications() { if (!this._nWindows && !this.window) return let monitor = this.dtpPanel.panelManager.notificationsMonitor let state = monitor.getState(this.app) let count = 0 if (!state) return if (SETTINGS.get_boolean('progress-show-count')) { this.iconAnimator[`${state.urgent ? 'add' : 'remove'}Animation`]( this.icon._iconBin, 'dance', ) if (state.total) { count = state.total > 9 ? '9+' : state.total this.dtpPanel.intellihide.revealAndHold(Hold.NOTIFY) } else this.dtpPanel.intellihide.release(Hold.NOTIFY) } this._notificationsCount = count this._maybeUpdateNumberOverlay() } _maybeUpdateNumberOverlay() { let visible = this._numberOverlayBin.visible let shouldBeVisible = (this._hotkeysOverlayActiveMode && this._numberHotkeysOverlayLabel > -1) || this._notificationsCount let showNotifications = this._notificationsCount && this._hotkeysOverlayActiveMode !== 'TEMPORARILY' let label = showNotifications ? this._notificationsCount : this._numberHotkeysOverlayLabel this._numberOverlayLabel[ `${showNotifications ? 'add' : 'remove'}_style_class_name` ]('notification-badge') if (showNotifications) { this._numberOverlayLabel.set_x_align(Clutter.ActorAlign.END) this._numberOverlayLabel.set_y_align(Clutter.ActorAlign.START) this._numberOverlayLabel.set_x_expand(true) this._numberOverlayLabel.set_y_expand(true) } else { this._numberOverlayLabel.set_x_align(Clutter.ActorAlign.START) this._numberOverlayLabel.set_y_align(Clutter.ActorAlign.START) this._numberOverlayLabel.set_x_expand(false) this._numberOverlayLabel.set_y_expand(false) } if (shouldBeVisible && label !== this._numberOverlayLabel.get_text()) { this._numberOverlayLabel.set_text(label.toString()) this._updateNumberOverlay() } if (visible && !shouldBeVisible) this._numberOverlayBin.hide() else if (!visible && shouldBeVisible) this._numberOverlayBin.show() } _numberOverlay() { // Add label for a numeric visual aid (hotkeys or notification) 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._numberHotkeysOverlayLabel = -1 this._numberOverlayBin.hide() this._dtpIconContainer.add_child(this._numberOverlayBin) } _updateNumberOverlay() { // 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 panelSize = this.dtpPanel.geom.iconSize let minFontSize = panelSize >= 32 ? 12 : 10 let fontSize = Math.round( Math.max(minFontSize, 0.2 * panelSize) / Utils.getScaleFactor(), ) let size = Math.round(fontSize * 1.3) let style = ` font-size: ${fontSize}px; height: ${size}px; ` this._numberOverlayLabel.set_style(style) } setHotkeysNumberOverlayLabel(number) { this._numberHotkeysOverlayLabel = number } toggleHotkeysNumberOverlay(activateMode) { this._hotkeysOverlayActiveMode = this._numberHotkeysOverlayLabel > -1 && activateMode this._maybeUpdateNumberOverlay() } handleDragOver(source) { if (source == Main.xdndHandler) { this._previewMenu.close(true) if (!this._nWindows && !this.window) return DND.DragMotionResult.MOVE_DROP if (this._nWindows == 1 || this.window) { this.window ? Main.activateWindow(this.window) : activateFirstWindow(this.app, this.monitor) } else this.dtpPanel.panelManager.showFocusedAppInOverview(this.app, true) return DND.DragMotionResult.MOVE_DROP } 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 GLib.SOURCE_REMOVE } 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() : Utils.getAllMetaWindows()).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 && (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(dtpPanel) { let panelSize = dtpPanel.geom.innerSize let padding = Taskbar.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 'App 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 if (source.window) this._quitAllItem = this.addAction('QuitAll', () => this._quitFromTaskbar(true), ) // replace quit item delete this._quitItem this._quitItem = this.addAction(_('Quit'), () => this._quitFromTaskbar()) } updateQuitItems() { let ungrouped = !!this.sourceActor.window let quitText = _('Quit') let count = getInterestingWindows( this._app, this.sourceActor.dtpPanel.monitor, ).length let quitMultipleText = ngettext( 'Quit %d Window', 'Quit %d Windows', count, ).format(count) if (ungrouped) { this._quitAllItem.label.set_text(quitMultipleText) this._quitAllItem.visible = count > 1 } else quitText = quitMultipleText this._quitItem.visible = count > 0 this._quitItem.label.set_text(quitText) } _quitFromTaskbar(all) { let time = global.get_current_time() let windows = !all && 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.geom.position let labelOffset = node.get_length('-x-offset') 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 = () => 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) 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, }) return this._iconActor } 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() { 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) this.actor.set_style( 'padding:' + padding + '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() { this.realShowAppsIcon.destroy() } } /** * A menu for the showAppsIcon */ export const MyShowAppsIconMenu = class extends PopupMenu.PopupMenu { constructor(actor, dtpPanel) { super(actor, 0, dtpPanel.geom.position) this._dtpPanel = dtpPanel this.updateItems(actor) } updateItems(sourceActor) { this.sourceActor = sourceActor this.removeAll() this._appendItem({ title: _('System Monitor'), cmd: ['gnome-system-monitor'], }) this._appendItem({ title: _('Files'), cmd: ['nautilus'], }) this._appendItem({ title: _('Settings'), cmd: ['gnome-control-center'], }) this._appendItem({ title: _('Zorin Appearance'), cmd: ['zorin-appearance'], }) 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(_('Taskbar 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 () { 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 }