summaryrefslogtreecommitdiff
path: root/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/appIcons.js
diff options
context:
space:
mode:
authornippy <you@example.com>2026-04-18 13:49:56 +0200
committernippy <you@example.com>2026-04-18 13:49:56 +0200
commit5d94c0a7d44a2255b81815a52a7056a94a39842d (patch)
tree759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/appIcons.js
parente79cdf210b267f21a186255ce1a4d50029439d54 (diff)
downloadRaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.tar.gz
RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.zip
update Raveos themes
Diffstat (limited to 'raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/appIcons.js')
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/appIcons.js1635
1 files changed, 1635 insertions, 0 deletions
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/appIcons.js b/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/appIcons.js
new file mode 100644
index 0000000..6b0ac24
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/appIcons.js
@@ -0,0 +1,1635 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+import {
+ Clutter,
+ Gio,
+ GLib,
+ GObject,
+ Meta,
+ Mtk,
+ Shell,
+ St,
+} from './dependencies/gi.js';
+
+import {
+ AppDisplay,
+ AppFavorites,
+ BoxPointer,
+ Dash,
+ Main,
+ PopupMenu,
+} from './dependencies/shell/ui.js';
+
+import {
+ ParentalControlsManager,
+ Util,
+} from './dependencies/shell/misc.js';
+
+import {
+ AppIconIndicators,
+ DBusMenuUtils,
+ Docking,
+ Locations,
+ Theming,
+ Utils,
+ WindowPreview,
+} from './imports.js';
+
+import {Extension} from './dependencies/shell/extensions/extension.js';
+
+// Use __ () and N__() for the extension gettext domain, and reuse
+// the shell domain with the default _() and N_()
+const {gettext: __, ngettext} = Extension;
+
+const DBusMenu = await DBusMenuUtils.haveDBusMenu();
+
+const tracker = Shell.WindowTracker.get_default();
+
+const Labels = Object.freeze({
+ ISOLATE_MONITORS: Symbol('isolate-monitors'),
+ ISOLATE_WORKSPACES: Symbol('isolate-workspaces'),
+ URGENT_WINDOWS: Symbol('urgent-windows'),
+});
+
+const clickAction = Object.freeze({
+ SKIP: 0,
+ MINIMIZE: 1,
+ LAUNCH: 2,
+ CYCLE_WINDOWS: 3,
+ MINIMIZE_OR_OVERVIEW: 4,
+ PREVIEWS: 5,
+ MINIMIZE_OR_PREVIEWS: 6,
+ FOCUS_OR_PREVIEWS: 7,
+ FOCUS_OR_APP_SPREAD: 8,
+ FOCUS_MINIMIZE_OR_PREVIEWS: 9,
+ FOCUS_MINIMIZE_OR_APP_SPREAD: 10,
+ QUIT: 11,
+});
+
+const scrollAction = Object.freeze({
+ DO_NOTHING: 0,
+ CYCLE_WINDOWS: 1,
+ SWITCH_WORKSPACE: 2,
+});
+
+// module "Dash" did not export DASH_ITEM_LABEL_SHOW_TIME, so let's define it.
+const DASH_ITEM_LABEL_SHOW_TIME = Dash.DASH_ITEM_LABEL_SHOW_TIME ?? 150;
+
+let recentlyClickedAppLoopId = 0;
+let recentlyClickedApp = null;
+let recentlyClickedAppWindows = null;
+let recentlyClickedAppIndex = 0;
+let recentlyClickedAppMonitor = -1;
+
+/**
+ * Extend AppIcon
+ *
+ * - Apply a css class based on the number of windows of each application (#N);
+ * - Customized indicators for running applications in place of 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
+ * - Update menu if open on windows change
+ */
+const DockAbstractAppIcon = GObject.registerClass({
+ GTypeFlags: GObject.TypeFlags.ABSTRACT,
+ Properties: {
+ 'focused': GObject.ParamSpec.boolean(
+ 'focused', 'focused', 'focused',
+ GObject.ParamFlags.READWRITE,
+ false),
+ 'running': GObject.ParamSpec.boolean(
+ 'running', 'running', 'running',
+ GObject.ParamFlags.READWRITE,
+ false),
+ 'urgent': GObject.ParamSpec.boolean(
+ 'urgent', 'urgent', 'urgent',
+ GObject.ParamFlags.READWRITE,
+ false),
+ 'updating': GObject.ParamSpec.boolean(
+ 'updating', 'updating', 'updating',
+ GObject.ParamFlags.READWRITE,
+ false),
+ 'windows-count': GObject.ParamSpec.uint(
+ 'windows-count', 'windows-count', 'windows-count',
+ GObject.ParamFlags.READWRITE,
+ 0, GLib.MAXUINT32, 0),
+ },
+}, class DockAbstractAppIcon extends Dash.DashIcon {
+ // settings are required inside.
+ _init(app, monitorIndex, iconAnimator) {
+ super._init(app);
+
+ // a prefix is required to avoid conflicting with the parent class variable
+ this.monitorIndex = monitorIndex;
+ this._signalsHandler = new Utils.GlobalSignalsHandler(this);
+ this.iconAnimator = iconAnimator;
+ this._indicator = new AppIconIndicators.AppIconIndicator(this);
+ this._urgentWindows = new Set();
+
+ // 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._signalsHandler.add(this.app, 'windows-changed', () => this._updateWindows());
+ this._signalsHandler.add(this.app, 'notify::state', () => this._updateRunningState());
+ this._signalsHandler.add(global.display, 'window-demands-attention', (_dpy, window) =>
+ this._onWindowDemandsAttention(window));
+ this._signalsHandler.add(global.display, 'window-marked-urgent', (_dpy, window) =>
+ this._onWindowDemandsAttention(window));
+
+ // In Wayland sessions, this signal is needed to track the state of windows dragged
+ // from one monitor to another. As this is triggered quite often (whenever a new
+ // window of any application opened or moved to a different desktop),
+ // we restrict this signal to the case when Labels.ISOLATE_MONITORS is true,
+ // and if there are at least 2 monitors.
+ if (Docking.DockManager.settings.isolateMonitors &&
+ Main.layoutManager.monitors.length > 1) {
+ this._signalsHandler.addWithLabel(Labels.ISOLATE_MONITORS,
+ global.display,
+ 'window-entered-monitor',
+ this._onWindowEntered.bind(this));
+ }
+
+ this.connect('notify::running', () => {
+ if (this.running)
+ this.add_style_class_name('running');
+ else
+ this.remove_style_class_name('running');
+ });
+ this.notify('running');
+
+ this.connect('notify::focused', () => {
+ if (this.focused)
+ this.add_style_class_name('focused');
+ else
+ this.remove_style_class_name('focused');
+ });
+ this.notify('focused');
+
+ this.connect('notify::updating', () => {
+ if (this.updating)
+ this.add_style_class_name('updating');
+ else
+ this.remove_style_class_name('updating');
+ });
+ this.notify('updating');
+
+ const {notificationsMonitor} = Docking.DockManager.getDefault();
+
+ this.connect('notify::urgent', () => {
+ const icon = this.icon._iconBin;
+ this._signalsHandler.removeWithLabel(Labels.URGENT_WINDOWS);
+ if (this.urgent) {
+ if (Docking.DockManager.settings.danceUrgentApplications &&
+ notificationsMonitor.enabled) {
+ icon.set_pivot_point(0.5, 0.5);
+ this.iconAnimator.addAnimation(icon, 'wiggle');
+ }
+ if (this.running && !this._urgentWindows.size) {
+ const urgentWindows = this.getInterestingWindows();
+ urgentWindows.forEach(w => (w._manualUrgency = true));
+ this._updateUrgentWindows(urgentWindows);
+ }
+ } else {
+ this.iconAnimator.removeAnimation(icon, 'wiggle');
+ icon.rotation_angle_z = 0;
+ this._urgentWindows.forEach(w => delete w._manualUrgency);
+ this._updateUrgentWindows();
+ }
+ });
+ this.notify('urgent');
+
+ this._progressOverlayArea = null;
+ this._progress = 0;
+
+ [
+ 'apply-custom-theme',
+ 'running-indicator-style',
+ 'show-icons-emblems',
+ 'show-icons-notifications-counter',
+ 'application-counter-overrides-notifications',
+ ].forEach(key => {
+ this._signalsHandler.add(
+ Docking.DockManager.settings,
+ `changed::${key}`, () => {
+ this._indicator.destroy();
+ this._indicator = new AppIconIndicators.AppIconIndicator(this);
+ }
+ );
+ });
+
+ this._signalsHandler.add(notificationsMonitor, 'state-changed', () => {
+ this._indicator.destroy();
+ this._indicator = new AppIconIndicators.AppIconIndicator(this);
+ });
+
+ this._updateState();
+ this._numberOverlay();
+
+ this._previewMenuManager = null;
+ this._previewMenu = null;
+ }
+
+ _onDestroy() {
+ super._onDestroy();
+
+ // This is necessary due to an upstream bug
+ // https://bugzilla.gnome.org/show_bug.cgi?id=757556
+ // It can be safely removed once it get solved upstream.
+ this._menu?.close(false);
+ delete this._menu;
+ }
+
+ ownsWindow(window) {
+ return this.app === tracker.get_window_app(window);
+ }
+
+ _onWindowEntered(metaScreen, monitorIndex, metaWin) {
+ if (this.ownsWindow(metaWin))
+ this._updateWindows();
+ }
+
+ vfunc_scroll_event(scrollEvent) {
+ const {settings} = Docking.DockManager;
+ const isEnabled = settings.scrollAction === scrollAction.CYCLE_WINDOWS;
+ if (!isEnabled)
+ return Clutter.EVENT_PROPAGATE;
+
+ // We only activate windows of running applications, i.e. we never open new windows
+ // We check if the app is running, and that the # of windows is > 0 in
+ // case we use workspace isolation,
+ if (!this.running)
+ return Clutter.EVENT_PROPAGATE;
+
+ if (this._optionalScrollCycleWindowsDeadTimeId) {
+ return Clutter.EVENT_PROPAGATE;
+ } else {
+ this._optionalScrollCycleWindowsDeadTimeId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT, 250, () => {
+ this._optionalScrollCycleWindowsDeadTimeId = 0;
+ });
+ }
+
+ let direction = null;
+
+ switch (scrollEvent.direction) {
+ case Clutter.ScrollDirection.UP:
+ direction = Meta.MotionDirection.UP;
+ break;
+ case Clutter.ScrollDirection.DOWN:
+ direction = Meta.MotionDirection.DOWN;
+ break;
+ case Clutter.ScrollDirection.SMOOTH: {
+ const [, dy] = Clutter.get_current_event().get_scroll_delta();
+ if (dy < 0)
+ direction = Meta.MotionDirection.UP;
+ else if (dy > 0)
+ direction = Meta.MotionDirection.DOWN;
+ }
+ break;
+ }
+
+ if (!Main.overview.visible) {
+ const reversed = direction === Meta.MotionDirection.UP;
+ if (this.focused && !this._urgentWindows.size) {
+ this._cycleThroughWindows(reversed);
+ } else {
+ // Activate the first window
+ const windows = this.getInterestingWindows();
+ if (windows.length > 0) {
+ const [w] = windows;
+ Main.activateWindow(w);
+ }
+ }
+ } else {
+ this.app.activate();
+ }
+ return Clutter.EVENT_STOP;
+ }
+
+ _updateWindows() {
+ if (this._menu && this._menu.isOpen)
+ this._menu.update();
+
+ this._updateState();
+ this.updateIconGeometry();
+ }
+
+ _updateState() {
+ this._urgentWindows.clear();
+ const interestingWindows = this.getInterestingWindows();
+ this.windowsCount = interestingWindows.length;
+ this._updateRunningState();
+ this._updateFocusState();
+ this._updateUrgentWindows(interestingWindows);
+
+ if (Docking.DockManager.settings.isolateWorkspaces) {
+ this._signalsHandler.removeWithLabel(Labels.ISOLATE_WORKSPACES);
+ interestingWindows.forEach(window =>
+ this._signalsHandler.addWithLabel(Labels.ISOLATE_WORKSPACES,
+ window, 'workspace-changed', () => this._updateWindows()));
+ }
+ }
+
+ _updateRunningState() {
+ this.running = (this.app.state === Shell.AppState.RUNNING) && this.windowsCount;
+ }
+
+ _updateFocusState() {
+ this.focused = tracker.focus_app === this.app && this.running;
+ }
+
+ _updateUrgentWindows(interestingWindows) {
+ this._signalsHandler.removeWithLabel(Labels.URGENT_WINDOWS);
+ this._urgentWindows.clear();
+ if (interestingWindows === undefined)
+ interestingWindows = this.getInterestingWindows();
+ interestingWindows.filter(isWindowUrgent).forEach(win => this._addUrgentWindow(win));
+ this.urgent = !!this._urgentWindows.size;
+ }
+
+ _onWindowDemandsAttention(window) {
+ if (this.ownsWindow(window) && isWindowUrgent(window))
+ this._addUrgentWindow(window);
+ }
+
+ _updateDotStyle() {
+ super._updateDotStyle();
+ const themeNode = this._dot.get_theme_node();
+ this._dot.translationX = themeNode.get_length('offset-x');
+ }
+
+ _addUrgentWindow(window) {
+ if (this._urgentWindows.has(window))
+ return;
+
+ if (window._manualUrgency && window.has_focus()) {
+ delete window._manualUrgency;
+ return;
+ }
+
+ this._urgentWindows.add(window);
+ this.urgent = true;
+
+ const onDemandsAttentionChanged = () => {
+ if (!isWindowUrgent(window))
+ this._updateUrgentWindows();
+ };
+
+ if (window.demandsAttention) {
+ this._signalsHandler.addWithLabel(Labels.URGENT_WINDOWS, window,
+ 'notify::demands-attention', () => onDemandsAttentionChanged());
+ }
+ if (window.urgent) {
+ this._signalsHandler.addWithLabel(Labels.URGENT_WINDOWS, window,
+ 'notify::urgent', () => onDemandsAttentionChanged());
+ }
+ if (window._manualUrgency) {
+ this._signalsHandler.addWithLabel(Labels.URGENT_WINDOWS, window,
+ 'focus', () => {
+ delete window._manualUrgency;
+ onDemandsAttentionChanged();
+ });
+ }
+ }
+
+ /**
+ * Update target for minimization animation
+ */
+ updateIconGeometry() {
+ // 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())
+ return;
+
+ const rect = new Mtk.Rectangle();
+
+ [rect.x, rect.y] = this.get_transformed_position();
+ [rect.width, rect.height] = this.get_transformed_size();
+
+ let windows = this.getWindows();
+ if (Docking.DockManager.settings.multiMonitor) {
+ const {monitorIndex} = this;
+ windows = windows.filter(w => w.get_monitor() === monitorIndex);
+ }
+ windows.forEach(w => w.set_icon_geometry(rect));
+ }
+
+ _updateRunningStyle() {
+ // The logic originally in this function has been moved to
+ // AppIconIndicatorBase._updateDefaultDot(). However it cannot be removed as
+ // it called by the parent constructor.
+ }
+
+ popupMenu() {
+ this._removeMenuTimeout?.();
+ this.fake_release();
+ this._draggable.fakeRelease?.();
+
+ if (!this._menu) {
+ this._menu = new DockAppIconMenu(this);
+ this._menu.connect('activate-window', (menu, window) => {
+ if (window) {
+ Main.activateWindow(window);
+ } else {
+ Main.overview.hide();
+ Main.panel.closeCalendar();
+ }
+ });
+ this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
+ if (!isPoppedUp) {
+ this._onMenuPoppedDown();
+ } else {
+ // Setting the max-height is s useful if part of the menu is
+ // scrollable so the minimum height is smaller than the natural height.
+ const monitorIndex = Main.layoutManager.findIndexForActor(this);
+ const workArea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
+ const position = Utils.getPosition();
+ const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
+ const isHorizontal = position === St.Side.TOP || position === St.Side.BOTTOM;
+ // If horizontal also remove the height of the dash
+ const {dockFixed: fixedDock} = Docking.DockManager.settings;
+ const additionalMargin = isHorizontal && !fixedDock ? Main.overview.dash.height : 0;
+ const verticalMargins = this._menu.actor.margin_top + this._menu.actor.margin_bottom;
+ const maxMenuHeight = workArea.height - additionalMargin - verticalMargins;
+ // Also set a max width to the menu, so long labels (long windows title) get truncated
+ this._menu.actor.style = 'max-width: 400px; ' +
+ `max-height: ${Math.round(maxMenuHeight / scaleFactor)}px;`;
+ }
+ });
+ const id = Main.overview.connect('hiding', () => {
+ this._menu.close();
+ });
+ this._menu.actor.connect('destroy', () => {
+ Main.overview.disconnect(id);
+ });
+
+ this._menuManager.addMenu(this._menu);
+ }
+
+ this.emit('menu-state-changed', true);
+
+ this.set_hover(true);
+ this._menu.popup();
+ // Removed in GNOME 50.
+ this._menuManager.ignoreRelease?.();
+ this.emit('sync-tooltip');
+
+ return false;
+ }
+
+ activate(button) {
+ const event = Clutter.get_current_event();
+ let modifiers = event ? event.get_state() : 0;
+
+ // Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.)
+ modifiers &= Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK;
+
+ // We don't change the CTRL-click behavior: in such case we just chain
+ // up the parent method and return.
+ if (modifiers & Clutter.ModifierType.CONTROL_MASK) {
+ // Keep default behavior: launch new window
+ // By calling the parent method I make it compatible
+ // with other extensions tweaking ctrl + click
+ super.activate(button);
+ return;
+ }
+
+ // 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;
+ const {settings} = Docking.DockManager;
+ if (button && button === 2) {
+ if (modifiers & Clutter.ModifierType.SHIFT_MASK)
+ buttonAction = settings.shiftMiddleClickAction;
+ else
+ buttonAction = settings.middleClickAction;
+ } else if (button && button === 1) {
+ if (modifiers & Clutter.ModifierType.SHIFT_MASK)
+ buttonAction = settings.shiftClickAction;
+ else
+ buttonAction = settings.clickAction;
+ }
+
+ switch (buttonAction) {
+ case clickAction.FOCUS_OR_APP_SPREAD:
+ if (!Docking.DockManager.getDefault().appSpread.supported)
+ buttonAction = clickAction.FOCUS_OR_PREVIEWS;
+ break;
+
+ case clickAction.FOCUS_MINIMIZE_OR_APP_SPREAD:
+ if (!Docking.DockManager.getDefault().appSpread.supported)
+ buttonAction = clickAction.FOCUS_MINIMIZE_OR_PREVIEWS;
+ break;
+ }
+
+ // We check if the app is running, and that the # of windows is > 0 in
+ // case we use workspace isolation.
+ const windows = this.getInterestingWindows();
+
+ // Some action modes (e.g. MINIMIZE_OR_OVERVIEW) require overview to remain open
+ // This variable keeps track of this
+ let shouldHideOverview = true;
+
+ // We customize the action only when the application is already running
+ if (this.running) {
+ const hasUrgentWindows = !!this._urgentWindows.size;
+ const singleOrUrgentWindows = windows.length === 1 || hasUrgentWindows;
+ switch (buttonAction) {
+ case clickAction.MINIMIZE:
+ // In overview just activate the app, unless the action is explicitly
+ // requested with a keyboard modifier
+ if (!Main.overview.visible || modifiers) {
+ // If we have button=2 or a modifier, allow minimization even if
+ // the app is not focused
+ if (this.focused && !hasUrgentWindows || 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 clickCount = 0;
+ if (Clutter.EventType.CLUTTER_BUTTON_PRESS)
+ clickCount = event.get_click_count();
+ const allWindows = (button === 1 && !modifiers) || clickCount > 1;
+ this._minimizeWindow(allWindows);
+ } else {
+ this._activateAllWindows();
+ }
+ } else {
+ const [w] = windows;
+ Main.activateWindow(w);
+ }
+ break;
+
+ case clickAction.MINIMIZE_OR_OVERVIEW:
+ // When a single window is present, toggle minimization
+ // If only one windows is present toggle minimization, but
+ // only when triggered with the simple click action
+ // (no modifiers, no middle click).
+ if (singleOrUrgentWindows && !modifiers && button === 1) {
+ const [w] = windows;
+ if (this.focused) {
+ if (buttonAction !== clickAction.FOCUS_OR_APP_SPREAD) {
+ // Window is raised, minimize it
+ this._minimizeWindow(w);
+ }
+ } else {
+ // Window is minimized, raise it
+ Main.activateWindow(w);
+ }
+ // Launch overview when multiple windows are present
+ // TODO: only show current app windows when gnome shell API will allow it
+ } else {
+ shouldHideOverview = false;
+ Main.overview.toggle();
+ }
+ break;
+
+ case clickAction.CYCLE_WINDOWS:
+ if (!Main.overview.visible) {
+ if (this.focused && !hasUrgentWindows) {
+ this._cycleThroughWindows();
+ } else {
+ // Activate the first window
+ const [w] = windows;
+ Main.activateWindow(w);
+ }
+ } else {
+ this.app.activate();
+ }
+ break;
+
+ case clickAction.FOCUS_OR_PREVIEWS:
+ if (this.focused && !hasUrgentWindows &&
+ (windows.length > 1 || modifiers || button !== 1)) {
+ this._windowPreviews();
+ } else {
+ // Activate the first window
+ const [w] = windows;
+ Main.activateWindow(w);
+ }
+ break;
+
+ case clickAction.FOCUS_MINIMIZE_OR_PREVIEWS:
+ if (this.focused && !hasUrgentWindows) {
+ if (windows.length > 1 || modifiers || button !== 1)
+ this._windowPreviews();
+ else if (!Main.overview.visible)
+ this._minimizeWindow();
+ } else {
+ // Activate the first window
+ const [w] = windows;
+ Main.activateWindow(w);
+ }
+ break;
+
+ case clickAction.LAUNCH:
+ this.launchNewWindow();
+ break;
+
+ case clickAction.PREVIEWS:
+ if (!Main.overview.visible) {
+ // If only one windows is present just switch to it,
+ // but only when triggered with the simple click action
+ // (no modifiers, no middle click).
+ if (singleOrUrgentWindows && !modifiers && button === 1) {
+ const [w] = windows;
+ Main.activateWindow(w);
+ } else {
+ this._windowPreviews();
+ }
+ } else {
+ this.app.activate();
+ }
+ break;
+
+ case clickAction.MINIMIZE_OR_PREVIEWS:
+ // When a single window is present, toggle minimization
+ // If only one windows is present toggle minimization, but only
+ // when triggered with the standard click action (no modifiers,
+ // no middle click).
+ if (!Main.overview.visible) {
+ if (singleOrUrgentWindows && !modifiers && button === 1) {
+ const [w] = windows;
+ if (this.focused) {
+ // Window is raised, minimize it
+ this._minimizeWindow(w);
+ } else {
+ // Window is minimized, raise it
+ Main.activateWindow(w);
+ }
+ } else {
+ // Launch previews when multiple windows are present
+ this._windowPreviews();
+ }
+ } else {
+ this.app.activate();
+ }
+ break;
+
+ case clickAction.FOCUS_OR_APP_SPREAD:
+ if (this.focused && !singleOrUrgentWindows && !modifiers && button === 1) {
+ shouldHideOverview = false;
+ Docking.DockManager.getDefault().appSpread.toggle(this.app);
+ } else {
+ // Activate the first window
+ Main.activateWindow(windows[0]);
+ }
+ break;
+
+ case clickAction.FOCUS_MINIMIZE_OR_APP_SPREAD:
+ if (this.focused && !singleOrUrgentWindows && !modifiers && button === 1) {
+ shouldHideOverview = false;
+ Docking.DockManager.getDefault().appSpread.toggle(this.app);
+ } else if (!this.focused) {
+ // Activate the first window
+ Main.activateWindow(windows[0]);
+ } else {
+ this._minimizeWindow();
+ }
+ break;
+
+ case clickAction.QUIT:
+ this.closeAllWindows();
+ break;
+
+ case clickAction.SKIP:
+ Main.activateWindow(windows[0]);
+ break;
+ }
+ } else {
+ this.launchNewWindow();
+ }
+
+ // Hide overview except when action mode requires it
+ if (shouldHideOverview)
+ Main.overview.hide();
+ }
+
+ shouldShowTooltip() {
+ return super.shouldShowTooltip() && !this._previewMenu?.isOpen &&
+ !Docking.DockManager.settings.hideTooltip;
+ }
+
+ _windowPreviews() {
+ if (!this._previewMenu) {
+ this._previewMenuManager = new PopupMenu.PopupMenuManager(this);
+
+ this._previewMenu = new WindowPreview.WindowPreviewMenu(this);
+
+ this._previewMenuManager.addMenu(this._previewMenu);
+
+ this._previewMenu.connect('open-state-changed', (menu, isPoppedUp) => {
+ if (!isPoppedUp)
+ this._onMenuPoppedDown();
+ });
+ const id = Main.overview.connect('hiding', () => {
+ this._previewMenu.close();
+ });
+ this._previewMenu.actor.connect('destroy', () => {
+ Main.overview.disconnect(id);
+ });
+ }
+
+ this.emit('menu-state-changed', !this._previewMenu.isOpen);
+
+ if (this._previewMenu.isOpen)
+ this._previewMenu.close();
+ else
+ this._previewMenu.popup();
+
+ return false;
+ }
+
+ // Try to do the right thing when attempting to launch a new window of an app. In
+ // particular, if the application doesn't allow to launch a new window, activate
+ // the existing window instead.
+ launchNewWindow() {
+ if (this.updating) {
+ const icon = Gio.Icon.new_for_string('action-unavailable-symbolic');
+ Main.osdWindowManager.show(-1, icon,
+ _('%s is updating, try again later').format(this.name),
+ null);
+ return;
+ }
+
+ if (this.app.state === Shell.AppState.RUNNING &&
+ this.app.can_open_new_window()) {
+ this.animateLaunch();
+ this.app.open_new_window(-1);
+ } else {
+ // Try to manually activate the first window. Otherwise, when the
+ // app is activated by switching to a different workspace, a launch
+ // spinning icon is shown and disappears only after a timeout.
+ const windows = this.getWindows();
+ if (windows.length > 0) {
+ Main.activateWindow(windows[0]);
+ } else {
+ this.app.activate();
+ this.animateLaunch();
+ }
+ }
+ }
+
+ _numberOverlay() {
+ // Add label for a Hot-Key visual aid
+ this._numberOverlayLabel = new St.Label();
+ this._numberOverlayBin = new St.Bin({
+ child: this._numberOverlayLabel,
+ x_align: Clutter.ActorAlign.START,
+ y_align: Clutter.ActorAlign.START,
+ x_expand: true, y_expand: true,
+ });
+ this._numberOverlayLabel.add_style_class_name('number-overlay');
+ this._numberOverlayOrder = -1;
+ this._numberOverlayBin.hide();
+
+ this._iconContainer.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.
+ const scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
+ // 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
+ const [minWidth_, natWidth] = this._iconContainer.get_preferred_width(-1);
+ const fontSize = Math.round(Math.max(12, 0.3 * natWidth) / scaleFactor);
+ const size = Math.round(fontSize * 1.2);
+ this._numberOverlayLabel.set_style(
+ `font-size: ${fontSize}px;` +
+ `border-radius: ${this.icon.iconSize}px;` +
+ `width: ${size}px; height: ${size}px;`
+ );
+ }
+
+ setNumberOverlay(number) {
+ this._numberOverlayOrder = number;
+ this._numberOverlayLabel.set_text(number.toString());
+ }
+
+ toggleNumberOverlay(activate) {
+ if (activate && this._numberOverlayOrder > -1) {
+ this.updateNumberOverlay();
+ this._numberOverlayBin.show();
+ } else {
+ this._numberOverlayBin.hide();
+ }
+ }
+
+ _minimizeWindow(param) {
+ // Param true make all app windows minimize
+ const windows = this.getInterestingWindows();
+ const currentWorkspace = global.workspace_manager.get_active_workspace();
+ for (let i = 0; i < windows.length; i++) {
+ const w = windows[i];
+ if (w.get_workspace() === currentWorkspace && 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.
+ _activateAllWindows() {
+ // First activate first window so workspace is switched if needed.
+ // We don't do this if isolation is on!
+ if (!Docking.DockManager.settings.isolateWorkspaces &&
+ !Docking.DockManager.settings.isolateMonitors) {
+ if (!this.running)
+ this.animateLaunch();
+ this.app.activate();
+ }
+
+ // then activate all other app windows in the current workspace
+ const windows = this.getInterestingWindows();
+ const activeWorkspace = global.workspace_manager.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]);
+ }
+ }
+
+ // This closes all windows of the app.
+ closeAllWindows() {
+ const windows = this.getInterestingWindows();
+ const time = global.get_current_time();
+ windows.forEach(w => w.delete(time));
+ }
+
+ _cycleThroughWindows(reversed) {
+ // Store for a little amount of time last clicked app and its windows
+ // since the order changes upon window interaction
+ const MEMORY_TIME = 3000;
+
+ const appWindows = this.getInterestingWindows();
+
+ if (appWindows.length < 1)
+ return;
+
+ if (recentlyClickedAppLoopId > 0)
+ GLib.source_remove(recentlyClickedAppLoopId);
+ recentlyClickedAppLoopId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT, MEMORY_TIME, this._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.
+ const monitorIsolation = Docking.DockManager.settings.isolateMonitors;
+ if (!recentlyClickedApp ||
+ recentlyClickedApp.get_id() !== this.app.get_id() ||
+ recentlyClickedAppWindows.length !== appWindows.length ||
+ (recentlyClickedAppMonitor !== this.monitorIndex && monitorIsolation)) {
+ recentlyClickedApp = this.app;
+ recentlyClickedAppWindows = appWindows;
+ recentlyClickedAppMonitor = this.monitorIndex;
+ recentlyClickedAppIndex = 0;
+ }
+
+ if (reversed) {
+ recentlyClickedAppIndex--;
+ if (recentlyClickedAppIndex < 0)
+ recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1;
+ } else {
+ recentlyClickedAppIndex++;
+ }
+ const index = recentlyClickedAppIndex % recentlyClickedAppWindows.length;
+ const window = recentlyClickedAppWindows[index];
+
+ Main.activateWindow(window);
+ }
+
+ _resetRecentlyClickedApp() {
+ if (recentlyClickedAppLoopId > 0)
+ GLib.source_remove(recentlyClickedAppLoopId);
+ recentlyClickedAppLoopId = 0;
+ recentlyClickedApp = null;
+ recentlyClickedAppWindows = null;
+ recentlyClickedAppIndex = 0;
+ recentlyClickedAppMonitor = -1;
+
+ return false;
+ }
+
+ getWindows() {
+ return this.app.get_windows();
+ }
+
+ // Filter out unnecessary windows, for instance
+ // nautilus desktop window.
+ getInterestingWindows() {
+ const interestingWindows = getInterestingWindows(this.getWindows(),
+ this.monitorIndex);
+
+ if (!this._urgentWindows.size)
+ return interestingWindows;
+
+ return [...new Set([...this._urgentWindows, ...interestingWindows])];
+ }
+
+ getSnapName() {
+ return this.app.appInfo?.get_string('X-SnapInstanceName');
+ }
+});
+
+const DockAppIcon = GObject.registerClass({
+}, class DockAppIcon extends DockAbstractAppIcon {
+ _init(app, monitorIndex, iconAnimator) {
+ super._init(app, monitorIndex, iconAnimator);
+
+ this._signalsHandler.add(tracker, 'notify::focus-app', () => this._updateFocusState());
+ }
+});
+
+const DockLocationAppIcon = GObject.registerClass({
+}, class DockLocationAppIcon extends DockAbstractAppIcon {
+ _init(app, monitorIndex, iconAnimator) {
+ if (!(app.appInfo instanceof Locations.LocationAppInfo))
+ throw new Error('Provided application %s is not a Location'.format(app));
+
+ super._init(app, monitorIndex, iconAnimator);
+
+ if (Docking.DockManager.settings.isolateLocations) {
+ this._signalsHandler.add(tracker, 'notify::focus-app', () => this._updateFocusState());
+ } else {
+ this._signalsHandler.add(global.display, 'notify::focus-window',
+ () => this._updateFocusState());
+ }
+
+ this._signalsHandler.add(this.app, 'notify::icon', () => this.icon.update());
+ }
+
+ get location() {
+ return this.app.location;
+ }
+
+ _updateFocusState() {
+ if (Docking.DockManager.settings.isolateLocations) {
+ super._updateFocusState();
+ return;
+ }
+
+ this.focused = this.app.isFocused && this.running;
+ }
+});
+
+/**
+ * @param app
+ * @param monitorIndex
+ * @param iconAnimator
+ */
+export function makeAppIcon(app, monitorIndex, iconAnimator) {
+ if (app.appInfo instanceof Locations.LocationAppInfo)
+ return new DockLocationAppIcon(app, monitorIndex, iconAnimator);
+
+ return new DockAppIcon(app, monitorIndex, iconAnimator);
+}
+
+/**
+ * DockAppIconMenu
+ *
+ * - set popup arrow side based on dash orientation
+ * - Add close windows option based on quitfromdash extension
+ * (https://github.com/deuill/shell-extension-quitfromdash)
+ * - Add open windows thumbnails instead of list
+ * - update menu when application windows change
+ */
+const DockAppIconMenu = class DockAppIconMenu extends PopupMenu.PopupMenu {
+ constructor(source) {
+ super(source, 0.5, Utils.getPosition());
+
+ this._signalsHandler = new Utils.GlobalSignalsHandler(this);
+
+ // We want to keep the item hovered while the menu is up
+ this.blockSourceEvents = true;
+
+ this.actor.add_style_class_name('app-menu');
+ this.actor.add_style_class_name('dock-app-menu');
+
+ // Chain our visibility and lifecycle to that of the source
+ this._signalsHandler.add(source, 'notify::mapped', () => {
+ if (!source.mapped)
+ this.close();
+ });
+ source.connect('destroy', () => this.destroy());
+
+ Main.uiGroup.add_child(this.actor);
+
+ const {remoteModel} = Docking.DockManager.getDefault();
+ const remoteModelApp = remoteModel?.lookupById(this.sourceActor?.app?.id);
+ if (remoteModelApp && DBusMenu) {
+ const [onQuickList, onDynamicSection] = Utils.splitHandler((sender,
+ {quicklist}, dynamicSection) => {
+ dynamicSection.removeAll();
+ if (quicklist) {
+ quicklist.get_children().forEach(remoteItem =>
+ dynamicSection.addMenuItem(
+ DBusMenuUtils.makePopupMenuItem(remoteItem, false)));
+ }
+ });
+
+ this._signalsHandler.add([
+ remoteModelApp,
+ 'quicklist-changed',
+ onQuickList,
+ ], [
+ this,
+ 'dynamic-section-changed',
+ onDynamicSection,
+ ]);
+ }
+ }
+
+ destroy() {
+ super.destroy();
+ delete this.sourceActor;
+ delete this._signalsHandler;
+ }
+
+ _appendSeparator() {
+ this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+ }
+
+ _appendMenuItem(labelText, params) {
+ const item = new PopupMenu.PopupMenuItem(labelText, params);
+ this.addMenuItem(item);
+ return item;
+ }
+
+ popup(_activatingButton) {
+ this._rebuildMenu();
+ this.open(BoxPointer.PopupAnimation.FULL);
+ }
+
+ removeAll() {
+ super.removeAll();
+
+ delete this._allWindowsMenuItem;
+ delete this._quitMenuItem;
+ }
+
+ _rebuildMenu() {
+ this.removeAll();
+
+ const appItemLabel = this.sourceActor.updating
+ ? _('%s is being updated…').format(this.sourceActor.name)
+ : this.sourceActor.name;
+ this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem(appItemLabel));
+
+ const {app} = this.sourceActor;
+
+ if (Docking.DockManager.settings.showWindowsPreview) {
+ // Display the app windows menu items and the separator between windows
+ // of the current desktop and other windows.
+ const windows = this.sourceActor.getInterestingWindows();
+
+ this._allWindowsMenuItem = new PopupMenu.PopupSubMenuMenuItem(__('All Windows'), false);
+ if (this._allWindowsMenuItem.menu?.actor)
+ this._allWindowsMenuItem.menu.actor.overlayScrollbars = true;
+ this._allWindowsMenuItem.hide();
+ if (windows.length > 0)
+ this.addMenuItem(this._allWindowsMenuItem);
+ } else {
+ const windows = this.sourceActor.getInterestingWindows();
+
+ if (windows.length > 0) {
+ this.addMenuItem(
+ /* Translators: This is the heading of a list of open windows */
+ new PopupMenu.PopupSeparatorMenuItem(_('Open Windows')));
+ }
+
+ windows.forEach(window => {
+ const title = window.title ? window.title : app.get_name();
+ const item = this._appendMenuItem(title);
+ item.connect('activate', () => {
+ this.emit('activate-window', window);
+ });
+ });
+ }
+
+ if (!app.is_window_backed()) {
+ this._appendSeparator();
+
+ const appInfo = app.get_app_info();
+ const actions = this.sourceActor.updating ? [] : appInfo.list_actions();
+ if (!this.sourceActor.updating &&
+ app.can_open_new_window() &&
+ actions.indexOf('new-window') === -1) {
+ const newMenuItem = this._appendMenuItem(_('New Window'));
+ newMenuItem.connect('activate', () => {
+ if (app.state === Shell.AppState.STOPPED)
+ this.sourceActor.animateLaunch();
+
+ app.open_new_window(-1);
+ this.emit('activate-window', null);
+ });
+ this._appendSeparator();
+ }
+
+ if (!this.sourceActor.updating &&
+ Docking.DockManager.getDefault().discreteGpuAvailable &&
+ app.state === Shell.AppState.STOPPED) {
+ const appPrefersNonDefaultGPU = appInfo.get_boolean('PrefersNonDefaultGPU');
+ const gpuPref = appPrefersNonDefaultGPU
+ ? Shell.AppLaunchGpu.DEFAULT
+ : Shell.AppLaunchGpu.DISCRETE;
+ const gpuMenuItem = this._appendMenuItem(appPrefersNonDefaultGPU
+ ? _('Launch using Integrated Graphics Card')
+ : _('Launch using Discrete Graphics Card'));
+ gpuMenuItem.connect('activate', () => {
+ this.sourceActor.animateLaunch();
+ app.launch(0, -1, gpuPref);
+ this.emit('activate-window', null);
+ });
+ }
+
+ for (let i = 0; i < actions.length; i++) {
+ const action = actions[i];
+ const item = this._appendMenuItem(appInfo.get_action_name(action));
+ item.sensitive = !appInfo.busy;
+ item.connect('activate', (emitter, event) => {
+ app.launch_action(action, event.get_time(), -1);
+ this.emit('activate-window', null);
+ });
+ }
+
+ const canFavorite = global.settings.is_writable('favorite-apps') &&
+ (this.sourceActor instanceof DockAppIcon) &&
+ ParentalControlsManager.getDefault().shouldShowApp(app.appInfo);
+
+ if (canFavorite) {
+ this._appendSeparator();
+
+ const isFavorite = AppFavorites.getAppFavorites().isFavorite(app.get_id());
+ if (isFavorite) {
+ const item = this._appendMenuItem(_('Unpin'));
+ item.connect('activate', () => {
+ const favs = AppFavorites.getAppFavorites();
+ favs.removeFavorite(app.get_id());
+ });
+ } else {
+ const item = this._appendMenuItem(__('Pin to Dock'));
+ item.connect('activate', () => {
+ const favs = AppFavorites.getAppFavorites();
+ favs.addFavorite(app.get_id());
+ });
+ }
+ }
+
+ if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop') &&
+ this.sourceActor instanceof DockAppIcon &&
+ !this.sourceActor.getSnapName()) {
+ this._appendSeparator();
+ const item = this._appendMenuItem(_('App Details'));
+ item.connect('activate', () => {
+ const id = app.get_id();
+ const args = GLib.Variant.new('(ss)', [id, '']);
+ Gio.DBus.get(Gio.BusType.SESSION, null,
+ (o, res) => {
+ const bus = Gio.DBus.get_finish(res);
+ bus.call('org.gnome.Software',
+ '/org/gnome/Software',
+ 'org.gtk.Actions', 'Activate',
+ GLib.Variant.new('(sava{sv})',
+ ['details', [args], null]),
+ null, 0, -1, null, null);
+ Main.overview.hide();
+ });
+ });
+ }
+
+ if (this.sourceActor instanceof DockAppIcon) {
+ const snapName = this.sourceActor.getSnapName();
+ const snapStore = snapName
+ ? Shell.AppSystem.get_default().lookup_app(
+ 'snap-store_snap-store.desktop') : null;
+
+ if (snapStore) {
+ this._appendSeparator();
+ const item = this._appendMenuItem(_('App Details'));
+ item.connect('activate', (_, event) => {
+ snapStore.activate_full(-1, event.get_time());
+ Util.spawnApp(
+ [...snapStore.appInfo.get_commandline().split(' '), snapName]);
+ Main.overview.hide();
+ });
+ }
+ }
+ }
+
+ // dynamic menu
+ const items = this._getMenuItems();
+ let i = items.length;
+ if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop'))
+ i -= 2;
+
+ if (global.settings.is_writable('favorite-apps'))
+ i -= 2;
+
+ if (i < 0)
+ i = 0;
+
+ const dynamicSection = new PopupMenu.PopupMenuSection();
+ this.addMenuItem(dynamicSection, i);
+ this.emit('dynamic-section-changed', dynamicSection);
+
+ // quit menu
+ this._appendSeparator();
+ this._quitMenuItem = this._appendMenuItem(_('Quit'));
+ this._quitMenuItem.connect('activate', () => this.sourceActor.closeAllWindows());
+
+ this.update();
+ }
+
+ // update menu content when application windows change. This is desirable as actions
+ // acting on windows (closing) are performed while the menu is shown.
+ update() {
+ // update, show or hide the quit menu
+ if (this.sourceActor.windowsCount > 0) {
+ if (this.sourceActor.windowsCount === 1) {
+ this._quitMenuItem.label.set_text(_('Quit'));
+ } else {
+ this._quitMenuItem.label.set_text(ngettext(
+ 'Quit %d Window', 'Quit %d Windows', this.sourceActor.windowsCount).format(
+ this.sourceActor.windowsCount));
+ }
+
+ this._quitMenuItem.actor.show();
+ } else {
+ this._quitMenuItem.actor.hide();
+ }
+
+ if (Docking.DockManager.settings.showWindowsPreview) {
+ const windows = this.sourceActor.getInterestingWindows();
+
+ // update, show, or hide the allWindows menu
+ // Check if there are new windows not already displayed. In such case,
+ // repopulate the allWindows menu. Windows removal is already handled
+ // by each preview being connected to the destroy signal
+ const oldWindows = this._allWindowsMenuItem.menu._getMenuItems().map(item => {
+ return item._window;
+ });
+
+ const newWindows = windows.filter(w =>
+ oldWindows.indexOf(w) < 0);
+ if (newWindows.length > 0) {
+ this._populateAllWindowMenu(windows);
+
+ // Try to set the width to that of the submenu.
+ // TODO: can't get the actual size, getting a bit less.
+ // Temporary workaround: add 15px to compensate
+ this._allWindowsMenuItem.width = this._allWindowsMenuItem.menu.actor.width + 15;
+ }
+
+ // The menu is created hidden and never hidden after being shown.
+ // Instead, a signal connected to its items destroy will set is
+ // insensitive if no more windows preview are shown.
+ if (windows.length > 0) {
+ this._allWindowsMenuItem.show();
+ this._allWindowsMenuItem.setSensitive(true);
+
+ if (Docking.DockManager.settings.defaultWindowsPreviewToOpen)
+ this._allWindowsMenuItem.menu.open();
+ }
+ }
+
+ // Update separators
+ this._getMenuItems().forEach(item => {
+ if ('label' in item)
+ this._updateSeparatorVisibility(item);
+ });
+ }
+
+ _populateAllWindowMenu(windows) {
+ this._allWindowsMenuItem.menu.removeAll();
+
+ if (windows.length > 0) {
+ const activeWorkspace = global.workspace_manager.get_active_workspace();
+ let separatorShown = windows[0].get_workspace() !== activeWorkspace;
+
+ for (let i = 0; i < windows.length; i++) {
+ const window = windows[i];
+ if (!separatorShown && window.get_workspace() !== activeWorkspace) {
+ this._allWindowsMenuItem.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+ separatorShown = true;
+ }
+
+ const item = new WindowPreview.WindowPreviewMenuItem(window,
+ St.Side.LEFT);
+ this._allWindowsMenuItem.menu.addMenuItem(item);
+ item.connect('activate', () => {
+ this.emit('activate-window', window);
+ });
+
+ // This is to achieve a more graceful transition when the last
+ // window is closed.
+ item.connect('destroy', () => {
+ // It's still counting the item just going to be destroyed
+ if (this._allWindowsMenuItem.menu._getMenuItems().length === 1)
+ this._allWindowsMenuItem.setSensitive(false);
+ });
+ }
+ }
+ }
+};
+
+/**
+ * @param w
+ */
+function isWindowUrgent(w) {
+ return w.urgent || w.demandsAttention || w._manualUrgency;
+}
+
+/**
+ * Filter out unnecessary windows, for instance
+ * nautilus desktop window.
+ *
+ * @param windows
+ * @param monitorIndex
+ */
+export function getInterestingWindows(windows, monitorIndex) {
+ const {settings} = Docking.DockManager;
+
+ // When using workspace isolation, we filter out windows
+ // that are neither in the current workspace nor marked urgent
+ if (settings.isolateWorkspaces) {
+ const showUrgent = settings.workspaceAgnosticUrgentWindows;
+ const activeWorkspace = global.workspace_manager.get_active_workspace();
+ windows = windows.filter(w => {
+ const inWorkspace = w.get_workspace() === activeWorkspace;
+ return inWorkspace || (showUrgent && isWindowUrgent(w));
+ });
+ }
+
+ if (settings.isolateMonitors && monitorIndex >= 0) {
+ windows = windows.filter(w => {
+ return w.get_monitor() === monitorIndex;
+ });
+ }
+
+ return windows.filter(w => !w.skipTaskbar);
+}
+
+/**
+ * A ShowAppsIcon improved class.
+ *
+ * - set label position based on dash orientation
+ * Note: we are am reusing most machinery of the appIcon class.
+ * - implement a popupMenu based on the AppIcon code
+ * Note: we are reusing most machinery of the appIcon class)
+ *
+ */
+
+export const DockShowAppsIcon = GObject.registerClass({
+ Signals: {
+ 'menu-state-changed': {param_types: [GObject.TYPE_BOOLEAN]},
+ 'sync-tooltip': {},
+ },
+}
+, class DockShowAppsIcon extends Dash.ShowAppsIcon {
+ _init(position) {
+ super._init();
+
+ // Re-use appIcon methods
+ const {prototype: appIconPrototype} = AppDisplay.AppIcon;
+ this.toggleButton.y_expand = false;
+ this.toggleButton.connect('popup-menu', () =>
+ appIconPrototype._onKeyboardPopupMenu.call(this));
+ this.toggleButton.connect('clicked', () =>
+ this._removeMenuTimeout());
+
+ this.reactive = true;
+ this.toggleButton.popupMenu = (...args) =>
+ this.popupMenu(...args);
+ this.toggleButton._setPopupTimeout = (...args) =>
+ this._setPopupTimeout(...args);
+ this.toggleButton._removeMenuTimeout = (...args) =>
+ this._removeMenuTimeout(...args);
+
+ this.label?.add_style_class_name(Theming.PositionStyleClass[position]);
+ if (Docking.DockManager.settings.customThemeShrink)
+ this.label?.add_style_class_name('shrink');
+
+ this._menu = null;
+ this._menuManager = new PopupMenu.PopupMenuManager(this);
+ this._menuTimeoutId = 0;
+
+ this._maybeEnablePopupGestures();
+ }
+
+ _createIcon(size) {
+ this._iconActor = super._createIcon(size);
+ this._iconActor.fallbackIconName = this._iconActor.iconName;
+ this._iconActor.fallbackGicon = this._iconActor.gicon;
+ this._iconActor.iconName = `view-app-grid-${Main.sessionMode.currentMode}-symbolic`;
+ return this._iconActor;
+ }
+
+ vfunc_leave_event(...args) {
+ return AppDisplay.AppIcon.prototype.vfunc_leave_event.call(
+ this.toggleButton, ...args);
+ }
+
+ vfunc_button_press_event(...args) {
+ try {
+ // TODO: Drop this when not supporting GNOME 48 (and older) anymore.
+ return AppDisplay.AppIcon.prototype.vfunc_button_press_event.call(
+ this.toggleButton, ...args);
+ } catch {
+ return Clutter.EVENT_PROPAGATE;
+ }
+ }
+
+ vfunc_touch_event(...args) {
+ try {
+ // TODO: Drop this when not supporting GNOME 48 (and older) anymore.
+ return AppDisplay.AppIcon.prototype.vfunc_touch_event.call(
+ this.toggleButton, ...args);
+ } catch {
+ return Clutter.EVENT_PROPAGATE;
+ }
+ }
+
+ showLabel(...args) {
+ itemShowLabel.call(this, ...args);
+ }
+
+ setForcedHighlight(...args) {
+ AppDisplay.AppIcon.prototype.setForcedHighlight.call(this, ...args);
+ }
+
+ _onMenuPoppedDown(...args) {
+ AppDisplay.AppIcon.prototype._onMenuPoppedDown.call(this, ...args);
+ }
+
+ _setPopupTimeout(...args) {
+ AppDisplay.AppIcon.prototype._setPopupTimeout.call(this, ...args);
+ }
+
+ _removeMenuTimeout(...args) {
+ AppDisplay.AppIcon.prototype._removeMenuTimeout?.call(this, ...args);
+ }
+
+ _hasPopupMenu() {
+ return Docking.DockManager.extension.uuid !== 'ubuntu-dock@ubuntu.com';
+ }
+
+ _maybeEnablePopupGestures() {
+ if (!this._hasPopupMenu())
+ return;
+
+ if (!Clutter.LongPressGesture || !Clutter.ClickGesture)
+ return;
+
+ const longPressGesture = new Clutter.LongPressGesture();
+ longPressGesture.connect('recognize', () => this.popupMenu());
+ this.add_action(longPressGesture);
+
+ const rightClickGesture = new Clutter.ClickGesture({
+ required_button: Clutter.BUTTON_SECONDARY,
+ recognize_on_press: true,
+ });
+ rightClickGesture.connect('recognize', () => this.popupMenu());
+ this.add_action(rightClickGesture);
+ }
+
+ popupMenu() {
+ if (!this._hasPopupMenu())
+ return false;
+
+ this._removeMenuTimeout();
+ this.toggleButton.fake_release();
+
+ if (!this._menu) {
+ this._menu = new DockShowAppsIconMenu(this);
+ this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
+ if (!isPoppedUp)
+ this._onMenuPoppedDown();
+ });
+ const id = Main.overview.connect('hiding', () => {
+ this._menu.close();
+ });
+ this._menu.actor.connect('destroy', () => {
+ Main.overview.disconnect(id);
+ });
+ this._menuManager.addMenu(this._menu);
+ }
+
+ this.emit('menu-state-changed', true);
+
+ this.toggleButton.set_hover(true);
+ this._menu.popup();
+ // Removed in GNOME 50.
+ this._menuManager.ignoreRelease?.();
+ this.emit('sync-tooltip');
+
+ return false;
+ }
+});
+
+
+/**
+ * A menu for the showAppsIcon
+ */
+class DockShowAppsIconMenu extends DockAppIconMenu {
+ _rebuildMenu() {
+ this.removeAll();
+
+ this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem(__('Dash to Dock')));
+
+ const item = this._appendMenuItem(_('Settings'));
+ item.connect('activate', () =>
+ Docking.DockManager.extension.openPreferences());
+ }
+}
+
+/**
+ * This function is used for both DockShowAppsIcon and DockDashItemContainer
+ */
+export function itemShowLabel() {
+ /* eslint-disable no-invalid-this */
+ // Check if the label is still present at all. When switching workspace, the
+ // item might have been destroyed in between.
+ if (!this._labelText || !this.label.get_stage())
+ return;
+
+ this.label.set_text(this._labelText);
+ this.label.opacity = 0;
+ this.label.show();
+
+ const [stageX, stageY] = this.get_transformed_position();
+ const node = this.label.get_theme_node();
+
+ const itemWidth = this.allocation.x2 - this.allocation.x1;
+ const itemHeight = this.allocation.y2 - this.allocation.y1;
+
+ const labelWidth = this.label.get_width();
+ const labelHeight = this.label.get_height();
+
+ let x, y, xOffset, yOffset;
+
+ const position = Utils.getPosition();
+ const labelOffset = node.get_length('-x-offset');
+
+ switch (position) {
+ case St.Side.LEFT:
+ yOffset = Math.floor((itemHeight - labelHeight) / 2);
+ y = stageY + yOffset;
+ xOffset = labelOffset;
+ x = stageX + this.get_width() + xOffset;
+ break;
+ case St.Side.RIGHT:
+ yOffset = Math.floor((itemHeight - labelHeight) / 2);
+ y = stageY + yOffset;
+ xOffset = labelOffset;
+ x = Math.round(stageX) - labelWidth - xOffset;
+ break;
+ case St.Side.TOP:
+ y = stageY + labelOffset + itemHeight;
+ xOffset = Math.floor((itemWidth - labelWidth) / 2);
+ x = stageX + xOffset;
+ break;
+ case St.Side.BOTTOM:
+ yOffset = labelOffset;
+ y = stageY - labelHeight - yOffset;
+ xOffset = Math.floor((itemWidth - labelWidth) / 2);
+ x = stageX + xOffset;
+ break;
+ }
+
+ // keep the label inside the screen border
+ // Only needed fot the x coordinate.
+
+ // Leave a few pixel gap
+ const gap = 5;
+ const 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.remove_all_transitions();
+ this.label.set_position(x, y);
+ this.label.ease({
+ opacity: 255,
+ duration: DASH_ITEM_LABEL_SHOW_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ /* eslint-enable no-invalid-this */
+}