diff options
| author | nippy <you@example.com> | 2026-04-18 13:49:56 +0200 |
|---|---|---|
| committer | nippy <you@example.com> | 2026-04-18 13:49:56 +0200 |
| commit | 5d94c0a7d44a2255b81815a52a7056a94a39842d (patch) | |
| tree | 759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/docking.js | |
| parent | e79cdf210b267f21a186255ce1a4d50029439d54 (diff) | |
| download | RaveOS-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/docking.js')
| -rw-r--r-- | raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/docking.js | 2698 |
1 files changed, 2698 insertions, 0 deletions
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/docking.js b/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/docking.js new file mode 100644 index 0000000..393b1de --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/docking.js @@ -0,0 +1,2698 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +import { + Clutter, + GLib, + Gio, + GObject, + Meta, + Shell, + St, +} from './dependencies/gi.js'; + +import { + AppMenu, + AppDisplay, + Layout, + Main, + OverviewControls, + PointerWatcher, + Workspace, + WorkspacesView, + WorkspaceSwitcherPopup, +} from './dependencies/shell/ui.js'; + +import { + AnimationUtils, +} from './dependencies/shell/misc.js'; + +import { + AppIconsDecorator, + AppSpread, + DockDash, + DesktopIconsIntegration, + FileManager1API, + Intellihide, + LauncherAPI, + Locations, + NotificationsMonitor, + Theming, + Utils, +} 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: __} = Extension; + +const {signals: Signals} = imports; + +const DOCK_DWELL_CHECK_INTERVAL = 100; +const ICON_ANIMATOR_DURATION = 3000; +const STARTUP_ANIMATION_TIME = 500; + +export const State = Object.freeze({ + HIDDEN: 0, + SHOWING: 1, + SHOWN: 2, + HIDING: 3, +}); + +const scrollAction = Object.freeze({ + DO_NOTHING: 0, + CYCLE_WINDOWS: 1, + SWITCH_WORKSPACE: 2, +}); + +const Labels = Object.freeze({ + INITIALIZE: Symbol('initialize'), + ISOLATION: Symbol('isolation'), + LOCATIONS: Symbol('locations'), + MAIN_DASH: Symbol('main-dash'), + OLD_DASH_CHANGES: Symbol('old-dash-changes'), + SETTINGS: Symbol('settings'), + STARTUP_ANIMATION: Symbol('startup-animation'), + WORKSPACE_SWITCH_SCROLL: Symbol('workspace-switch-scroll'), +}); + +/** + * A simple St.Widget with one child whose allocation takes into account the + * slide out of its child via the slide-x property ([0:1]). + * + * Required since I want to track the input region of this container which is + * based on its allocation even if the child overflows the parent actor. By doing + * this the region of the dash that is slide-out is not stealing anymore the input + * regions making the extension usable when the primary monitor is the right one. + * + * The slide-x parameter can be used to directly animate the sliding. The parent + * must have a WEST (SOUTH) anchor_point to achieve the sliding to the RIGHT (BOTTOM) + * side. + */ +const DashSlideContainer = GObject.registerClass({ + Properties: { + 'monitor-index': GObject.ParamSpec.uint( + 'monitor-index', 'monitor-index', 'monitor-index', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + 0, GLib.MAXUINT32, 0), + 'side': GObject.ParamSpec.enum( + 'side', 'side', 'side', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + St.Side, St.Side.LEFT), + 'slide-x': GObject.ParamSpec.double( + 'slide-x', 'slide-x', 'slide-x', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + 0, 1, 1), + }, +}, class DashSlideContainer extends St.Bin { + _init(params = {}) { + super._init(params); + + this._slideoutSize = 0; // minimum size when slided out + this.connect('notify::slide-x', () => this.queue_relayout()); + + if (this.side === St.Side.TOP && DockManager.settings.dockFixed) { + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + this._signalsHandler.add(Main.panel, 'notify::height', + () => this.queue_relayout()); + } + } + + vfunc_allocate(box) { + const contentBox = this.get_theme_node().get_content_box(box); + + this.set_allocation(box); + + if (!this.child) + return; + + const availWidth = contentBox.x2 - contentBox.x1; + let availHeight = contentBox.y2 - contentBox.y1; + const [, , natChildWidth, natChildHeight] = + this.child.get_preferred_size(); + + const childWidth = natChildWidth; + const childHeight = natChildHeight; + + const childBox = new Clutter.ActorBox(); + + const slideoutSize = this._slideoutSize; + + if (this.side === St.Side.LEFT) { + childBox.x1 = (this.slideX - 1) * (childWidth - slideoutSize); + childBox.x2 = slideoutSize + this.slideX * (childWidth - slideoutSize); + childBox.y1 = 0; + childBox.y2 = childBox.y1 + childHeight; + } else if ((this.side === St.Side.RIGHT) || (this.side === St.Side.BOTTOM)) { + childBox.x1 = 0; + childBox.x2 = childWidth; + childBox.y1 = 0; + childBox.y2 = childBox.y1 + childHeight; + } else if (this.side === St.Side.TOP) { + const monitor = Main.layoutManager.monitors[this.monitorIndex]; + let yOffset = 0; + if (Main.panel.x === monitor.x && Main.panel.y === monitor.y && + DockManager.settings.dockFixed) + yOffset = Main.panel.height; + childBox.x1 = 0; + childBox.x2 = childWidth; + childBox.y1 = (this.slideX - 1) * (childHeight - slideoutSize) + yOffset; + childBox.y2 = slideoutSize + this.slideX * (childHeight - slideoutSize) + yOffset; + availHeight += yOffset; + } + + this.child.allocate(childBox); + + this.child.set_clip(-childBox.x1, -childBox.y1, + -childBox.x1 + availWidth, -childBox.y1 + availHeight); + } + + /** + * Just the child width but taking into account the slided out part + * + * @param forHeight + */ + vfunc_get_preferred_width(forHeight) { + let [minWidth, natWidth] = super.vfunc_get_preferred_width(forHeight || 0); + if ((this.side === St.Side.LEFT) || (this.side === St.Side.RIGHT)) { + minWidth = (minWidth - this._slideoutSize) * this.slideX + this._slideoutSize; + natWidth = (natWidth - this._slideoutSize) * this.slideX + this._slideoutSize; + } + return [minWidth, natWidth]; + } + + /** + * Just the child height but taking into account the slided out part + * + * @param forWidth + */ + vfunc_get_preferred_height(forWidth) { + let [minHeight, natHeight] = super.vfunc_get_preferred_height(forWidth || 0); + if ((this.side === St.Side.TOP) || (this.side === St.Side.BOTTOM)) { + minHeight = (minHeight - this._slideoutSize) * this.slideX + this._slideoutSize; + natHeight = (natHeight - this._slideoutSize) * this.slideX + this._slideoutSize; + + if (this.side === St.Side.TOP && DockManager.settings.dockFixed) { + const monitor = Main.layoutManager.monitors[this.monitorIndex]; + if (Main.panel.x === monitor.x && Main.panel.y === monitor.y) { + minHeight += Main.panel.height; + natHeight += Main.panel.height; + } + } + } + return [minHeight, natHeight]; + } +}); + +const DockedDash = GObject.registerClass({ + Properties: { + 'is-main': GObject.ParamSpec.boolean( + 'is-main', 'is-main', 'is-main', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + false), + 'monitor-index': GObject.ParamSpec.uint( + 'monitor-index', 'monitor-index', 'monitor-index', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + 0, GLib.MAXUINT32, 0), + }, + Signals: { + 'showing': {}, + 'hiding': {}, + }, +}, class DashToDock extends St.Bin { + _init(params) { + this._position = Utils.getPosition(); + + // This is the centering actor + super._init({ + ...params, + name: 'dashtodockContainer', + reactive: false, + style_class: Theming.PositionStyleClass[this._position], + }); + + if (this.monitorIndex === undefined) { + // Hello turkish locale, gjs has instead defined this.monitorIndex + // See: https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/742 + this.monitorIndex = this.monitor_index; + } + + this._rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL; + + // Load settings + const {settings} = DockManager; + this._isHorizontal = (this._position === St.Side.TOP) || (this._position === St.Side.BOTTOM); + + // Temporary ignore hover events linked to autohide for whatever reason + this._ignoreHover = false; + this._oldIgnoreHover = null; + // This variables are linked to the settings regardles of autohide or intellihide + // being temporary disable. Get set by _updateVisibilityMode; + this._autohideIsEnabled = null; + this._intellihideIsEnabled = null; + + // This variable marks if _disableUnredirect() is called + // to help restore the original state when intelihide is disabled. + this._unredirectDisabled = false; + + // Create intellihide object to monitor windows overlapping + this._intellihide = new Intellihide.Intellihide(this.monitorIndex); + + // initialize dock state + this._dockState = State.HIDDEN; + + // Put dock on the required monitor + this._monitor = Main.layoutManager.monitors[this.monitorIndex]; + + // this store size and the position where the dash is shown; + // used by intellihide module to check window overlap. + this.staticBox = new Clutter.ActorBox(); + + // Initialize pressure barrier variables + this._canUsePressure = false; + this._pressureBarrier = null; + this._barrier = null; + this._removeBarrierTimeoutId = 0; + + // Initialize dwelling system variables + this._dockDwelling = false; + this._dockWatch = null; + this._dockDwellUserTime = 0; + this._dockDwellTimeoutId = 0; + + // Create a new dash object + this.dash = new DockDash.DockDash(this.monitorIndex); + + if (Main.overview.isDummy || !settings.showShowAppsButton) + this.dash.hideShowAppsButton(); + + // Create the containers for sliding in and out and + // centering, turn on track hover + // This is the sliding actor whose allocation is to be tracked for input regions + this._slider = new DashSlideContainer({ + monitor_index: this._monitor.index, + side: this._position, + slide_x: Main.layoutManager._startingUp ? 0 : 1, + ...this._isHorizontal ? { + x_align: Clutter.ActorAlign.CENTER, + } : { + y_align: Clutter.ActorAlign.CENTER, + }, + }); + + // This is the actor whose hover status us tracked for autohide + this._box = new St.BoxLayout({ + name: 'dashtodockBox', + reactive: true, + track_hover: true, + }); + this._box.connect('notify::hover', this._hoverChanged.bind(this)); + + // Connect global signals + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + this._bindSettingsChanges(); + this._signalsHandler.add([ + // update when workarea changes, for instance if other extensions modify the struts + // (like moving th panel at the bottom) + global.display, + 'workareas-changed', + this._resetPosition.bind(this), + ], [ + global.display, + 'in-fullscreen-changed', + this._updateBarrier.bind(this), + ], [ + // Monitor windows overlapping + this._intellihide, + 'status-changed', + this._updateDashVisibility.bind(this), + ], [ + this.dash, + 'menu-opened', + () => { + this._onMenuOpened(); + }, + ], [ + // sync hover after a popupmenu is closed + this.dash, + 'menu-closed', + () => { + this._onMenuClosed(); + }, + ], [ + this.dash, + 'notify::requires-visibility', + () => this._updateDashVisibility(), + ]); + + if (!Main.overview.isDummy) { + this._signalsHandler.add([ + Main.overview, + 'item-drag-begin', + this._onDragStart.bind(this), + ], [ + Main.overview, + 'item-drag-end', + this._onDragEnd.bind(this), + ], [ + Main.overview, + 'item-drag-cancelled', + this._onDragEnd.bind(this), + ], [ + Main.overview, + 'showing', + this._onOverviewShowing.bind(this), + ], [ + Main.overview, + 'hiding', + this._onOverviewHiding.bind(this), + ], + [ + Main.overview, + 'hidden', + this._onOverviewHidden.bind(this), + ]); + } + + this._themeManager = new Theming.ThemeManager(this); + this._signalsHandler.add(this._themeManager, 'updated', + () => this.dash.resetAppIcons()); + + this._signalsHandler.add(DockManager.iconTheme, 'changed', + () => this.dash.resetAppIcons()); + + // Since the actor is not a topLevel child and its parent is now not added to the Chrome, + // the allocation change of the parent container (slide in and slideout) doesn't trigger + // anymore an update of the input regions. Force the update manually. + this.connect('notify::allocation', + Main.layoutManager._queueUpdateRegions.bind(Main.layoutManager)); + + + // Since Clutter has no longer ClutterAllocationFlags, + // "allocation-changed" signal has been removed. MR !1245 + this.dash._container.connect('notify::allocation', this._updateStaticBox.bind(this)); + this._slider.connect(this._isHorizontal ? 'notify::x' : 'notify::y', + this._updateStaticBox.bind(this)); + + // Load optional features that need to be activated for one dock only + if (this.isMain) + this._enableExtraFeatures(); + // Load optional features that need to be activated once per dock + this._optionalScrollWorkspaceSwitch(); + + // Delay operations that require the shell to be fully loaded and with + // user theme applied. + + this._signalsHandler.addWithLabel(Labels.INITIALIZE, global.stage, + 'after-paint', () => this._initialize()); + + // Add dash container actor and the container to the Chrome. + this.set_child(this._slider); + this._slider.set_child(this._box); + this._box.add_child(this.dash); + + // Add aligning container without tracking it for input region + this._trackDock(); + + // Create and apply height/width constraint to the dash. + if (this._isHorizontal) { + this.connect('notify::width', () => { + this.dash.setMaxSize(this.width, this.height); + }); + } else { + this.connect('notify::height', () => { + this.dash.setMaxSize(this.width, this.height); + }); + } + + if (this._position === St.Side.RIGHT) { + this.connect('notify::width', () => + (this.translation_x = -this.width)); + } else if (this._position === St.Side.BOTTOM) { + this.connect('notify::height', () => + (this.translation_y = -this.height)); + } + + // Set initial position + this._resetPosition(); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + get position() { + return this._position; + } + + get isHorizontal() { + return this._isHorizontal; + } + + _untrackDock() { + Main.layoutManager.untrackChrome(this); + } + + _trackDock() { + if (DockManager.settings.dockFixed) { + if (this.get_parent()) + Main.layoutManager.removeChrome(this); + Main.layoutManager.addChrome(this, { + trackFullscreen: true, + affectsStruts: true, + }); + } else { + if (this.get_parent()) + Main.layoutManager.removeChrome(this); + Main.layoutManager.addChrome(this); + } + } + + _initialize() { + this._signalsHandler.removeWithLabel(Labels.INITIALIZE); + + // Apply custom css class according to the settings + this._themeManager.updateCustomTheme(); + + this._updateVisibilityMode(); + + // In case we are already inside the overview when the extension is loaded, + // for instance on unlocking the screen if it was locked with the overview open. + if (Main.overview.visibleTarget) + this._onOverviewShowing(); + + this._updateAutoHideBarriers(); + } + + _onDestroy() { + // The dash, intellihide and themeManager have global signals as well internally + this.dash.destroy(); + this._intellihide.destroy(); + this._themeManager.destroy(); + + if (this._marginLater) { + Utils.laterRemove(this._marginLater); + delete this._marginLater; + } + + if (this._triggerTimeoutId) + GLib.source_remove(this._triggerTimeoutId); + + this._restoreUnredirect(); + + // Remove barrier timeout + if (this._removeBarrierTimeoutId > 0) + GLib.source_remove(this._removeBarrierTimeoutId); + + // Remove existing barrier + this._removeBarrier(); + + // Remove pointer watcher + if (this._dockWatch) { + PointerWatcher.getPointerWatcher()._removeWatch(this._dockWatch); + this._dockWatch = null; + } + } + + _updateAutoHideBarriers() { + // Remove pointer watcher + if (this._dockWatch) { + PointerWatcher.getPointerWatcher()._removeWatch(this._dockWatch); + this._dockWatch = null; + } + + // Setup pressure barrier (GS38+ only) + this._updatePressureBarrier(); + this._updateBarrier(); + + // setup dwelling system if pressure barriers are not available + this._setupDockDwellIfNeeded(); + } + + _bindSettingsChanges() { + const {settings} = DockManager; + this._signalsHandler.add([ + settings, + 'changed::scroll-action', + () => { + this._optionalScrollWorkspaceSwitch(); + }, + ], [ + settings, + 'changed::dash-max-icon-size', + () => { + this.dash.setIconSize(settings.dashMaxIconSize); + }, + ], [ + settings, + 'changed::icon-size-fixed', + () => { + this.dash.setIconSize(settings.dashMaxIconSize); + }, + ], [ + settings, + 'changed::show-favorites', + () => { + this.dash.resetAppIcons(); + }, + ], [ + settings, + 'changed::show-trash', + () => { + this.dash.resetAppIcons(); + }, + Utils.SignalsHandlerFlags.CONNECT_AFTER, + ], [ + settings, + 'changed::show-mounts', + () => { + this.dash.resetAppIcons(); + }, + Utils.SignalsHandlerFlags.CONNECT_AFTER, + ], [ + settings, + 'changed::isolate-locations', + () => this.dash.resetAppIcons(), + Utils.SignalsHandlerFlags.CONNECT_AFTER, + ], [ + settings, + 'changed::dance-urgent-applications', + () => this.dash.resetAppIcons(), + Utils.SignalsHandlerFlags.CONNECT_AFTER, + ], [ + settings, + 'changed::show-running', + () => { + this.dash.resetAppIcons(); + }, + ], [ + settings, + 'changed::show-apps-always-in-the-edge', + () => { + this.dash.updateShowAppsButton(); + }, + ], [ + settings, + 'changed::show-apps-at-top', + () => { + this.dash.updateShowAppsButton(); + }, + ], [ + settings, + 'changed::show-show-apps-button', + () => { + if (!Main.overview.isDummy && + settings.showShowAppsButton) + this.dash.showShowAppsButton(); + else + this.dash.hideShowAppsButton(); + }, + ], [ + settings, + 'changed::dock-fixed', + () => { + this._untrackDock(); + this._trackDock(); + + this._resetPosition(); + this._updateAutoHideBarriers(); + this._updateVisibilityMode(); + }, + ], [ + settings, + 'changed::manualhide', + () => { + this._updateVisibilityMode(); + }, + ], [ + settings, + 'changed::intellihide', + () => { + this._updateVisibilityMode(); + this._updateVisibleDesktop(); + }, + ], [ + settings, + 'changed::intellihide-mode', + () => { + this._intellihide.forceUpdate(); + }, + ], [ + settings, + 'changed::autohide', + () => { + this._updateVisibilityMode(); + this._updateAutoHideBarriers(); + }, + ], [ + settings, + 'changed::autohide-in-fullscreen', + this._updateBarrier.bind(this), + ], [ + settings, + 'changed::show-dock-urgent-notify', + () => { + this.dash.resetAppIcons(); + }, + ], + [ + settings, + 'changed::extend-height', + this._resetPosition.bind(this), + ], [ + settings, + 'changed::height-fraction', + this._resetPosition.bind(this), + ], [ + settings, + 'changed::always-center-icons', + () => this.dash.resetAppIcons(), + ], [ + settings, + 'changed::require-pressure-to-show', + () => this._updateAutoHideBarriers(), + ], [ + settings, + 'changed::pressure-threshold', + () => { + this._updatePressureBarrier(); + this._updateBarrier(); + }, + ]); + } + + _disableUnredirect() { + if (!this._unredirectDisabled) { + if (Meta.disable_unredirect_for_display !== undefined) + Meta.disable_unredirect_for_display(global.display); + else if (global.compositor.disable_unredirect !== undefined) + global.compositor.disable_unredirect(); + this._unredirectDisabled = true; + } + } + + _restoreUnredirect() { + if (this._unredirectDisabled) { + if (Meta.enable_unredirect_for_display !== undefined) + Meta.enable_unredirect_for_display(global.display); + else if (global.compositor.enable_unredirect !== undefined) + global.compositor.enable_unredirect(); + this._unredirectDisabled = false; + } + } + + /** + * This is call when visibility settings change + */ + _updateVisibilityMode() { + const {settings} = DockManager; + if (DockManager.settings.dockFixed || DockManager.settings.manualhide) { + this._autohideIsEnabled = false; + this._intellihideIsEnabled = false; + } else { + this._autohideIsEnabled = settings.autohide; + this._intellihideIsEnabled = settings.intellihide; + } + + if (this._autohideIsEnabled) + this.add_style_class_name('autohide'); + else + this.remove_style_class_name('autohide'); + + if (this._intellihideIsEnabled) { + this._intellihide.enable(); + } else { + this._intellihide.disable(); + this._restoreUnredirect(); + } + + this._updateDashVisibility(); + } + + /** + * Show/hide dash based on, in order of priority: + * overview visibility + * fixed mode + * intellihide + * autohide + * overview visibility + */ + _updateDashVisibility() { + if (DockManager.settings.manualhide) { + this._ignoreHover = true; + this._removeAnimations(); + this._animateOut(0, 0); + return; + } + + if (Main.overview.visibleTarget) + return; + + const {settings} = DockManager; + + if (DockManager.settings.dockFixed) { + this._removeAnimations(); + this._animateIn(settings.animationTime, 0); + } else if (this._intellihideIsEnabled) { + if (!this.dash.requiresVisibility && this._intellihide.getOverlapStatus()) { + this._ignoreHover = false; + // Do not hide if autohide is enabled and mouse is hover + if (!this._box.hover || !this._autohideIsEnabled) + this._animateOut(settings.animationTime, 0); + } else { + this._ignoreHover = true; + this._removeAnimations(); + this._animateIn(settings.animationTime, 0); + } + } else if (this._autohideIsEnabled) { + this._ignoreHover = false; + + if (this._box.hover || this.dash.requiresVisibility) + this._animateIn(settings.animationTime, 0); + else + this._animateOut(settings.animationTime, 0); + } else { + this._animateOut(settings.animationTime, 0); + } + } + + _onOverviewShowing() { + this.add_style_class_name('overview'); + + this._ignoreHover = true; + this._intellihide.disable(); + this._removeAnimations(); + this._animateIn(DockManager.settings.animationTime, 0); + } + + _onOverviewHiding() { + this._intellihide.enable(); + this._updateDashVisibility(); + } + + _onOverviewHidden() { + this.remove_style_class_name('overview'); + this._updateDashVisibility(); + } + + _onMenuOpened() { + this._ignoreHover = true; + } + + _onMenuClosed() { + this._ignoreHover = false; + this._box.sync_hover(); + this._updateDashVisibility(); + } + + _hoverChanged() { + if (!this._ignoreHover) { + // Skip if dock is not in autohide mode for instance because it is shown + // by intellihide. + if (this._autohideIsEnabled) { + if (this._box.hover || Main.overview.visible) + this._show(); + else + this._hide(); + } + } + } + + getDockState() { + return this._dockState; + } + + _show() { + this._delayedHide = false; + if ((this._dockState === State.HIDDEN) || (this._dockState === State.HIDING)) { + if (this._dockState === State.HIDING) + // suppress all potential queued transitions - i.e. added but not started, + // always give priority to show + this._removeAnimations(); + + this.emit('showing'); + this._animateIn(DockManager.settings.animationTime, 0); + } + } + + _hide() { + // If no hiding animation is running or queued + if ((this._dockState === State.SHOWN) || (this._dockState === State.SHOWING)) { + const {settings} = DockManager; + const delay = settings.hideDelay; + + if (this._dockState === State.SHOWING) { + // if a show already started, let it finish; queue hide without removing the show. + // to obtain this, we wait for the animateIn animation to be completed + this._delayedHide = true; + return; + } + + this.emit('hiding'); + this._animateOut(settings.animationTime, delay); + } + } + + _animateIn(time, delay) { + if (this._intellihideIsEnabled) + this._disableUnredirect(); + this._dockState = State.SHOWING; + this.dash.iconAnimator.start(); + this._delayedHide = false; + + this._slider.ease_property('slide-x', 1, { + duration: time * 1000, + delay: delay * 1000, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._dockState = State.SHOWN; + // Remove barrier so that mouse pointer is released and can + // monitors on other side of dock. + // NOTE: Delay needed to keep mouse from moving past dock and + // re-hiding dock immediately. This gives users an opportunity + // to hover over the dock + if (this._removeBarrierTimeoutId > 0) + GLib.source_remove(this._removeBarrierTimeoutId); + + if (!this._delayedHide) { + this._removeBarrierTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 100, this._removeBarrier.bind(this)); + } else { + this._hide(); + } + }, + }); + } + + _animateOut(time, delay) { + this._dockState = State.HIDING; + + this._slider.ease_property('slide-x', 0, { + duration: time * 1000, + delay: delay * 1000, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._dockState = State.HIDDEN; + if (this._intellihideIsEnabled) + this._restoreUnredirect(); + // Remove queued barrier removal timeout if any + if (this._removeBarrierTimeoutId > 0) + GLib.source_remove(this._removeBarrierTimeoutId); + this._updateBarrier(); + this.dash.iconAnimator.pause(); + }, + }); + } + + /** + * Dwelling system based on the GNOME Shell 3.14 messageTray code. + */ + _setupDockDwellIfNeeded() { + // If we don't have extended barrier features, then we need + // to support the old tray dwelling mechanism. + if (this._autohideIsEnabled && + (!Utils.supportsExtendedBarriers() || + !DockManager.settings.requirePressureToShow)) { + const pointerWatcher = PointerWatcher.getPointerWatcher(); + this._dockWatch = pointerWatcher.addWatch( + DOCK_DWELL_CHECK_INTERVAL, this._checkDockDwell.bind(this)); + this._dockDwelling = false; + this._dockDwellUserTime = 0; + } + } + + _checkDockDwell(x, y) { + const workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitor.index); + let shouldDwell; + // Check for the correct screen edge, extending the sensitive area to the whole workarea, + // minus 1 px to avoid conflicting with other active corners. + if (this._position === St.Side.LEFT) { + shouldDwell = (x === this._monitor.x) && (y > workArea.y) && + (y < workArea.y + workArea.height); + } else if (this._position === St.Side.RIGHT) { + shouldDwell = (x === this._monitor.x + this._monitor.width - 1) && + (y > workArea.y) && (y < workArea.y + workArea.height); + } else if (this._position === St.Side.TOP) { + shouldDwell = (y === this._monitor.y) && (x > workArea.x) && + (x < workArea.x + workArea.width); + } else if (this._position === St.Side.BOTTOM) { + shouldDwell = (y === this._monitor.y + this._monitor.height - 1) && + (x > workArea.x) && (x < workArea.x + workArea.width); + } + + if (shouldDwell) { + // We only set up dwell timeout when the user is not hovering over the dock + // already (!this._box.hover). + // The _dockDwelling variable is used so that we only try to + // fire off one dock dwell - if it fails (because, say, the user has the mouse down), + // we don't try again until the user moves the mouse up and down again. + if (!this._dockDwelling && !this._box.hover && (this._dockDwellTimeoutId === 0)) { + // Save the interaction timestamp so we can detect user input + const focusWindow = global.display.focus_window; + this._dockDwellUserTime = focusWindow ? focusWindow.user_time : 0; + + this._dockDwellTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + DockManager.settings.showDelay * 1000, + this._dockDwellTimeout.bind(this)); + GLib.Source.set_name_by_id(this._dockDwellTimeoutId, + '[dash-to-dock] this._dockDwellTimeout'); + } + this._dockDwelling = true; + } else { + this._cancelDockDwell(); + this._dockDwelling = false; + } + } + + _cancelDockDwell() { + if (this._dockDwellTimeoutId !== 0) { + GLib.source_remove(this._dockDwellTimeoutId); + this._dockDwellTimeoutId = 0; + } + } + + _dockDwellTimeout() { + this._dockDwellTimeoutId = 0; + + if (!DockManager.settings.autohideInFullscreen && + this._monitor.inFullscreen) + return GLib.SOURCE_REMOVE; + + // We don't want to open the tray when a modal dialog + // is up, so we check the modal count for that. When we are in the + // overview we have to take the overview's modal push into account + if (Main.modalCount > (Main.overview.visible ? 1 : 0)) + return GLib.SOURCE_REMOVE; + + // If the user interacted with the focus window since we started the tray + // dwell (by clicking or typing), don't activate the message tray + const focusWindow = global.display.focus_window; + const currentUserTime = focusWindow ? focusWindow.user_time : 0; + if (currentUserTime !== this._dockDwellUserTime) + return GLib.SOURCE_REMOVE; + + // Reuse the pressure version function, the logic is the same + this._onPressureSensed(); + return GLib.SOURCE_REMOVE; + } + + _updatePressureBarrier() { + const {settings} = DockManager; + this._canUsePressure = Utils.supportsExtendedBarriers(); + const {pressureThreshold} = settings; + + // Remove existing pressure barrier + if (this._pressureBarrier) { + this._pressureBarrier.destroy(); + this._pressureBarrier = null; + } + + if (this._barrier) { + this._barrier.destroy(); + this._barrier = null; + } + + // Create new pressure barrier based on pressure threshold setting + if (this._canUsePressure && this._autohideIsEnabled && + DockManager.settings.requirePressureToShow) { + this._pressureBarrier = new Layout.PressureBarrier( + pressureThreshold, settings.showDelay * 1000, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW); + this._pressureBarrier.connect('trigger', _barrier => { + if (!settings.autohideInFullscreen && this._monitor.inFullscreen) + return; + this._onPressureSensed(); + }); + } + } + + /** + * handler for mouse pressure sensed + */ + _onPressureSensed() { + if (Main.overview.visibleTarget) + return; + + if (this._triggerTimeoutId) + GLib.source_remove(this._triggerTimeoutId); + + // In case the mouse move away from the dock area before hovering it, + // in such case the leave event would never be triggered and the dock + // would stay visible forever. + this._triggerTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => { + const [x, y, mods_] = global.get_pointer(); + let shouldHide = true; + switch (this._position) { + case St.Side.LEFT: + if (x <= this.staticBox.x2 && + x >= this._monitor.x && + y >= this._monitor.y && + y <= this._monitor.y + this._monitor.height) + shouldHide = false; + + break; + case St.Side.RIGHT: + if (x >= this.staticBox.x1 && + x <= this._monitor.x + this._monitor.width && + y >= this._monitor.y && + y <= this._monitor.y + this._monitor.height) + shouldHide = false; + + break; + case St.Side.TOP: + if (x >= this._monitor.x && + x <= this._monitor.x + this._monitor.width && + y <= this.staticBox.y2 && + y >= this._monitor.y) + shouldHide = false; + + break; + case St.Side.BOTTOM: + if (x >= this._monitor.x && + x <= this._monitor.x + this._monitor.width && + y >= this.staticBox.y1 && + y <= this._monitor.y + this._monitor.height) + shouldHide = false; + } + if (shouldHide) { + this._triggerTimeoutId = 0; + this._hoverChanged(); + return GLib.SOURCE_REMOVE; + } else { + return GLib.SOURCE_CONTINUE; + } + }); + + this._show(); + } + + /** + * Remove pressure barrier + */ + _removeBarrier() { + if (this._barrier) { + if (this._pressureBarrier) + this._pressureBarrier.removeBarrier(this._barrier); + this._barrier.destroy(); + this._barrier = null; + } + this._removeBarrierTimeoutId = 0; + return false; + } + + /** + * Update pressure barrier size + */ + _updateBarrier() { + // Remove existing barrier + this._removeBarrier(); + + // The barrier needs to be removed in fullscreen with autohide disabled + // otherwise the mouse can get trapped on monitor. + if (this._monitor.inFullscreen && + !DockManager.settings.autohideInFullscreen) + return; + + // Manually reset pressure barrier + // This is necessary because we remove the pressure barrier when it is + // triggered to show the dock + if (this._pressureBarrier) { + this._pressureBarrier._reset(); + this._pressureBarrier._isTriggered = false; + } + + // Create new barrier + // The barrier extends to the whole workarea, minus 1 px to avoid + // conflicting with other active corners + // Note: dash in fixed position doesn't use pressure barrier. + if (this._canUsePressure && this._autohideIsEnabled && + DockManager.settings.requirePressureToShow) { + let x1, x2, y1, y2, direction; + const workArea = Main.layoutManager.getWorkAreaForMonitor( + this._monitor.index); + + if (this._position === St.Side.LEFT) { + x1 = this._monitor.x + 1; + x2 = x1; + y1 = workArea.y + 1; + y2 = workArea.y + workArea.height - 1; + direction = Meta.BarrierDirection.POSITIVE_X; + } else if (this._position === St.Side.RIGHT) { + x1 = this._monitor.x + this._monitor.width - 1; + x2 = x1; + y1 = workArea.y + 1; + y2 = workArea.y + workArea.height - 1; + direction = Meta.BarrierDirection.NEGATIVE_X; + } else if (this._position === St.Side.TOP) { + x1 = workArea.x + 1; + x2 = workArea.x + workArea.width - 1; + y1 = this._monitor.y; + y2 = y1; + direction = Meta.BarrierDirection.POSITIVE_Y; + } else if (this._position === St.Side.BOTTOM) { + x1 = workArea.x + 1; + x2 = workArea.x + workArea.width - 1; + y1 = this._monitor.y + this._monitor.height; + y2 = y1; + direction = Meta.BarrierDirection.NEGATIVE_Y; + } + + if (this._pressureBarrier && this._dockState === State.HIDDEN) { + this._barrier = new Meta.Barrier({ + backend: global.backend, + x1, + x2, + y1, + y2, + directions: direction, + }); + this._pressureBarrier.addBarrier(this._barrier); + } + } + } + + _isPrimaryMonitor() { + return this.monitorIndex === Main.layoutManager.primaryIndex; + } + + _resetPosition() { + // Ensure variables linked to settings are updated. + this._updateVisibilityMode(); + + const {dockFixed: fixedIsEnabled, dockExtended: extendHeight} = DockManager.settings; + + if (fixedIsEnabled) + this.add_style_class_name('fixed'); + else + this.remove_style_class_name('fixed'); + + // Note: do not use the workarea coordinates in the direction on which the dock is placed, + // to avoid a loop [position change -> workArea change -> position change] with + // fixed dock. + const workArea = Main.layoutManager.getWorkAreaForMonitor(this.monitorIndex); + + let fraction = DockManager.settings.heightFraction; + if (extendHeight) + fraction = 1; + else if ((fraction < 0) || (fraction > 1)) + fraction = 0.95; + + if (this._isHorizontal) { + this.width = Math.round(fraction * workArea.width); + + let posY = this._monitor.y; + if (this._position === St.Side.BOTTOM) + posY += this._monitor.height; + + this.x = workArea.x + Math.round((1 - fraction) / 2 * workArea.width); + this.y = posY; + + if (extendHeight) { + this.dash._container.set_width(this.width); + this.add_style_class_name('extended'); + } else { + this.dash._container.set_width(-1); + this.remove_style_class_name('extended'); + } + } else { + this.height = Math.round(fraction * workArea.height); + + let posX = this._monitor.x; + if (this._position === St.Side.RIGHT) + posX += this._monitor.width; + + this.x = posX; + this.y = workArea.y + Math.round((1 - fraction) / 2 * workArea.height); + + if (extendHeight) { + this.dash._container.set_height(this.height); + this.add_style_class_name('extended'); + } else { + this.dash._container.set_height(-1); + this.remove_style_class_name('extended'); + } + } + } + + _updateVisibleDesktop() { + if (!this._intellihideIsEnabled) + return; + + const {desktopIconsUsableArea} = DockManager.getDefault(); + if (this._position === St.Side.BOTTOM) + desktopIconsUsableArea.setMargins(this.monitorIndex, 0, this._box.height, 0, 0); + else if (this._position === St.Side.TOP) + desktopIconsUsableArea.setMargins(this.monitorIndex, this._box.height, 0, 0, 0); + else if (this._position === St.Side.RIGHT) + desktopIconsUsableArea.setMargins(this.monitorIndex, 0, 0, 0, this._box.width); + else if (this._position === St.Side.LEFT) + desktopIconsUsableArea.setMargins(this.monitorIndex, 0, 0, this._box.width, 0); + } + + _updateStaticBox() { + this.staticBox.init_rect( + this.x + this._slider.x - (this._position === St.Side.RIGHT ? this._box.width : 0), + this.y + this._slider.y - (this._position === St.Side.BOTTOM ? this._box.height : 0), + this._box.width, + this._box.height + ); + + this._intellihide.updateTargetBox(this.staticBox); + this._updateVisibleDesktop(); + } + + _removeAnimations() { + this._slider.remove_all_transitions(); + } + + _onDragStart() { + this._oldIgnoreHover = this._ignoreHover; + this._ignoreHover = true; + this._animateIn(DockManager.settings.animationTime, 0); + } + + _onDragEnd() { + if (this._oldIgnoreHover) + this._ignoreHover = this._oldIgnoreHover; + this._oldIgnoreHover = null; + this._box.sync_hover(); + this._updateDashVisibility(); + } + + /** + * Show dock and give key focus to it + */ + _onAccessibilityFocus() { + this._box.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); + this._animateIn(DockManager.settings.animationTime, 0); + } + + // Optional features to be enabled only for the main Dock + _enableExtraFeatures() { + // Restore dash accessibility + Main.ctrlAltTabManager.addGroup( + this.dash, _('Dash'), 'user-bookmarks-symbolic', + {focusCallback: this._onAccessibilityFocus.bind(this)}); + } + + /** + * Switch workspace by scrolling over the dock + */ + _optionalScrollWorkspaceSwitch() { + const isEnabled = () => + DockManager.settings.scrollAction === scrollAction.SWITCH_WORKSPACE; + + const enable = () => { + this._signalsHandler.removeWithLabel(Labels.WORKSPACE_SWITCH_SCROLL); + + this._signalsHandler.addWithLabel(Labels.WORKSPACE_SWITCH_SCROLL, + this._box, 'scroll-event', (_, e) => onScrollEvent(e)); + }; + + const disable = () => { + this._signalsHandler.removeWithLabel(Labels.WORKSPACE_SWITCH_SCROLL); + + if (this._optionalScrollWorkspaceSwitchDeadTimeId) { + GLib.source_remove(this._optionalScrollWorkspaceSwitchDeadTimeId); + this._optionalScrollWorkspaceSwitchDeadTimeId = 0; + } + }; + + DockManager.settings.connect('changed::scroll-action', () => { + if (isEnabled()) + enable(); + else + disable(); + }); + + if (isEnabled()) + enable(); + + // This was inspired to desktop-scroller@obsidien.github.com + const onScrollEvent = event => { + // When in overview change workspace only in windows view + if (Main.overview.visible) + return false; + + const activeWs = global.workspace_manager.get_active_workspace(); + let direction = null; + + let prevDirection, nextDirection; + if (global.workspace_manager.layout_columns > global.workspace_manager.layout_rows) { + prevDirection = Meta.MotionDirection.UP; + nextDirection = Meta.MotionDirection.DOWN; + } else { + prevDirection = Meta.MotionDirection.LEFT; + nextDirection = Meta.MotionDirection.RIGHT; + } + + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.UP: + direction = prevDirection; + break; + case Clutter.ScrollDirection.DOWN: + direction = nextDirection; + break; + case Clutter.ScrollDirection.SMOOTH: { + const [dx_, dy] = event.get_scroll_delta(); + if (dy < 0) + direction = prevDirection; + else if (dy > 0) + direction = nextDirection; + } + break; + } + + if (direction) { + // Prevent scroll events from triggering too many workspace switches + // by adding a 250ms dead time between each scroll event. + // Useful on laptops when using a touch pad. + + // During the deadtime do nothing + if (this._optionalScrollWorkspaceSwitchDeadTimeId) { + return false; + } else { + this._optionalScrollWorkspaceSwitchDeadTimeId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 250, () => { + this._optionalScrollWorkspaceSwitchDeadTimeId = 0; + }); + } + + let ws; + + ws = activeWs.get_neighbor(direction); + + if (!Main.wm._workspaceSwitcherPopup) { + // Support Workspace Grid extension showing their custom + // Grid Workspace Switcher + if (global.workspace_manager.workspace_grid !== undefined) { + Main.wm._workspaceSwitcherPopup = + global.workspace_manager.workspace_grid.getWorkspaceSwitcherPopup(); + } else { + Main.wm._workspaceSwitcherPopup = new WorkspaceSwitcherPopup.WorkspaceSwitcherPopup(); + } + } + // Set the actor non reactive, so that it doesn't prevent the + // clicks events from reaching the dash actor. I can't see a reason + // why it should be reactive. + Main.wm._workspaceSwitcherPopup.reactive = false; + Main.wm._workspaceSwitcherPopup.connect('destroy', () => { + Main.wm._workspaceSwitcherPopup = null; + }); + + // If Workspace Grid is installed, let them handle the scroll behavior. + if (global.workspace_manager.workspace_grid !== undefined) + ws = global.workspace_manager.workspace_grid.actionMoveWorkspace(direction); + else + Main.wm.actionMoveWorkspace(ws); + + // Do not show workspaceSwitcher in overview + if (!Main.overview.visible) + Main.wm._workspaceSwitcherPopup.display(ws.index()); + + return true; + } else { + return false; + } + }; + } + + _activateApp(appIndex) { + const children = this.dash._box.get_children().filter(actor => { + return actor.child && + actor.child.app; + }); + + // Apps currently in the dash + const apps = children.map(actor => { + return actor.child; + }); + + // Activate with button = 1, i.e. same as left click + const button = 1; + if (appIndex < apps.length) + apps[appIndex].activate(button); + } +}); + +/* + * Handle keyboard shortcuts + */ +const NUM_HOTKEYS = 10; + +const KeyboardShortcuts = class DashToDockKeyboardShortcuts { + constructor() { + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._hotKeysEnabled = false; + if (DockManager.settings.hotKeys) + this._enableHotKeys(); + + this._signalsHandler.add([ + DockManager.settings, + 'changed::hot-keys', + () => { + if (DockManager.settings.hotKeys) + this._enableHotKeys.bind(this)(); + else + this._disableHotKeys.bind(this)(); + }, + ]); + + this._optionalNumberOverlay(); + } + + destroy() { + DockManager.allDocks.forEach(dock => { + if (dock._numberOverlayTimeoutId) { + GLib.source_remove(dock._numberOverlayTimeoutId); + dock._numberOverlayTimeoutId = 0; + } + }); + + // Remove keybindings + this._disableHotKeys(); + this._disableExtraShortcut(); + this._signalsHandler.destroy(); + } + + _enableHotKeys() { + if (this._hotKeysEnabled) + return; + + // Setup keyboard bindings for dash elements + const keys = ['app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-']; + const {mainDock} = DockManager.getDefault(); + keys.forEach(function (key) { + for (let i = 0; i < NUM_HOTKEYS; i++) { + const appNum = i; + Main.wm.addKeybinding(key + (i + 1), DockManager.settings, + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + () => { + mainDock._activateApp(appNum); + this._showOverlay(); + }); + } + }, this); + + this._hotKeysEnabled = true; + } + + _disableHotKeys() { + if (!this._hotKeysEnabled) + return; + + const keys = ['app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-']; + keys.forEach(key => { + for (let i = 0; i < NUM_HOTKEYS; i++) + Main.wm.removeKeybinding(key + (i + 1)); + }, this); + + this._hotKeysEnabled = false; + } + + _optionalNumberOverlay() { + const {settings} = DockManager; + this._shortcutIsSet = false; + // Enable extra shortcut if either 'overlay' or 'show-dock' are true + if (settings.hotKeys && + (settings.hotkeysOverlay || settings.hotkeysShowDock)) + this._enableExtraShortcut(); + + this._signalsHandler.add([ + settings, + 'changed::hot-keys', + this._checkHotkeysOptions.bind(this), + ], [ + settings, + 'changed::hotkeys-overlay', + this._checkHotkeysOptions.bind(this), + ], [ + settings, + 'changed::hotkeys-show-dock', + this._checkHotkeysOptions.bind(this), + ]); + } + + _checkHotkeysOptions() { + const {settings} = DockManager; + + if (settings.hotKeys && + (settings.hotkeysOverlay || settings.hotkeysShowDock)) + this._enableExtraShortcut(); + else + this._disableExtraShortcut(); + } + + _enableExtraShortcut() { + if (!this._shortcutIsSet) { + Main.wm.addKeybinding('shortcut', DockManager.settings, + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + this._showOverlay.bind(this)); + this._shortcutIsSet = true; + } + } + + _disableExtraShortcut() { + if (this._shortcutIsSet) { + Main.wm.removeKeybinding('shortcut'); + this._shortcutIsSet = false; + } + } + + _showOverlay() { + for (const dock of DockManager.allDocks) { + if (DockManager.settings.hotkeysOverlay) + dock.dash.toggleNumberOverlay(true); + + // Restart the counting if the shortcut is pressed again + if (dock._numberOverlayTimeoutId) { + GLib.source_remove(dock._numberOverlayTimeoutId); + dock._numberOverlayTimeoutId = 0; + } + + // Hide the overlay/dock after the timeout + const timeout = DockManager.settings.shortcutTimeout * 1000; + dock._numberOverlayTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, timeout, () => { + dock._numberOverlayTimeoutId = 0; + dock.dash.toggleNumberOverlay(false); + // Hide the dock again if necessary + dock._updateDashVisibility(); + }); + + // Show the dock if it is hidden + if (DockManager.settings.hotkeysShowDock) { + const showDock = dock._intellihideIsEnabled || dock._autohideIsEnabled; + if (showDock) + dock._show(); + } + } + } +}; + +/** + * Isolate overview to open new windows for inactive apps + * Note: the future implementation is not fully contained here. + * Some bits are around in other methods of other classes. + * This class just take care of enabling/disabling the option. + */ +const WorkspaceIsolation = class DashToDockWorkspaceIsolation { + constructor() { + const {settings} = DockManager; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._injectionsHandler = new Utils.InjectionsHandler(); + + const updateAllDocks = () => { + DockManager.allDocks.forEach(dock => + dock.dash.resetAppIcons()); + if (settings.isolateWorkspaces || + settings.isolateMonitors) + this._enable.bind(this)(); + else + this._disable.bind(this)(); + }; + this._signalsHandler.add( + [settings, 'changed::isolate-workspaces', updateAllDocks], + [settings, 'changed::workspace-agnostic-urgent-windows', updateAllDocks], + [settings, 'changed::isolate-monitors', updateAllDocks] + ); + + if (settings.isolateWorkspaces || + settings.isolateMonitors) + this._enable(); + } + + _enable() { + // ensure I never double-register/inject + // although it should never happen + this._disable(); + + DockManager.allDocks.forEach(dock => { + this._signalsHandler.addWithLabel( + Labels.ISOLATION, + [global.display, 'restacked', () => dock.dash._queueRedisplay()], + [global.display, 'window-marked-urgent', () => dock.dash._queueRedisplay()], + [global.display, 'window-demands-attention', () => dock.dash._queueRedisplay()], + [global.window_manager, 'switch-workspace', () => dock.dash._queueRedisplay()] + ); + + // This last signal is only needed for monitor isolation, as windows + // might migrate from one monitor to another without triggering 'restacked' + if (DockManager.settings.isolateMonitors) { + this._signalsHandler.addWithLabel(Labels.ISOLATION, + global.display, + 'window-entered-monitor', + dock.dash._queueRedisplay.bind(dock.dash)); + } + }, this); + + /** + * here this is the Shell.App + */ + function IsolatedOverview() { + // These lines take care of Nautilus for icons on Desktop + const activeWorkspaceIndex = + global.workspaceManager.get_active_workspace_index(); + const windows = this.get_windows().filter(w => + !w.skipTaskbar && w.get_workspace().index() === activeWorkspaceIndex); + + if (windows.length) + return Main.activateWindow(windows[0]); + return this.open_new_window(-1); + } + + this._injectionsHandler.addWithLabel(Labels.ISOLATION, + Shell.App.prototype, + 'activate', + IsolatedOverview); + } + + _disable() { + this._signalsHandler.removeWithLabel(Labels.ISOLATION); + this._injectionsHandler.removeWithLabel(Labels.ISOLATION); + } + + destroy() { + this._signalsHandler.destroy(); + this._injectionsHandler.destroy(); + } +}; + + +export class DockManager { + constructor(extension) { + if (DockManager._singleton) + throw new Error('DashToDock has been already initialized'); + DockManager._singleton = this; + this._extension = extension; + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + this._methodInjections = new Utils.InjectionsHandler(this); + this._vfuncInjections = new Utils.VFuncInjectionsHandler(this); + this._propertyInjections = new Utils.PropertyInjectionsHandler(this); + this._settings = this._extension.getSettings( + 'org.gnome.shell.extensions.dash-to-dock'); + this._appSwitcherSettings = new Gio.Settings({schema_id: 'org.gnome.shell.app-switcher'}); + this._mapSettingsValues(); + + this._iconTheme = new St.IconTheme(); + + this._desktopIconsUsableArea = new DesktopIconsIntegration.DesktopIconsUsableAreaClass(); + this._oldDash = Main.overview.isDummy ? null : Main.overview.dash; + this._discreteGpuAvailable = AppDisplay.discreteGpuAvailable; + this._appSpread = new AppSpread.AppSpread(); + this._notificationsMonitor = new NotificationsMonitor.NotificationsMonitor(); + + const needsRemoteModel = () => + !this._notificationsMonitor.dndMode && this._settings.showIconsEmblems; + + const ensureRemoteModel = () => { + if (needsRemoteModel && !this._remoteModel) { + this._remoteModel = new LauncherAPI.LauncherEntryRemoteModel(); + this._appIconsDecorator = new AppIconsDecorator.AppIconsDecorator(); + } else if (!needsRemoteModel) { + this._remoteModel?.destroy(); + delete this._remoteModel; + this._appIconsDecorator?.destroy(); + delete this._appIconsDecorator; + } + }; + ensureRemoteModel(); + + this._notificationsMonitor.connect('changed', ensureRemoteModel); + this._settings.connect('changed::show-icons-emblems', ensureRemoteModel); + + if (this._discreteGpuAvailable === undefined) { + const updateDiscreteGpuAvailable = () => { + const switcherooProxy = global.get_switcheroo_control(); + if (switcherooProxy) { + const prop = switcherooProxy.get_cached_property('HasDualGpu'); + this._discreteGpuAvailable = prop?.unpack() ?? false; + } else { + this._discreteGpuAvailable = false; + } + }; + this._signalsHandler.add(global, 'notify::switcheroo-control', + () => updateDiscreteGpuAvailable()); + updateDiscreteGpuAvailable(); + } + + // Connect relevant signals to the toggling function + this._bindSettingsChanges(); + + this._ensureLocations(); + + /* Array of all the docks created */ + this._allDocks = []; + this._createDocks(); + + this._overrideAppMenus(); + + // status variable: true when the overview is shown through the dash + // applications button. + this._forcedOverview = false; + } + + static getDefault() { + return DockManager._singleton; + } + + static get allDocks() { + return DockManager.getDefault()._allDocks; + } + + static get extension() { + return DockManager.getDefault().extension; + } + + static get settings() { + return DockManager.getDefault().settings; + } + + get extension() { + return this._extension; + } + + get settings() { + return this._settings; + } + + static get iconTheme() { + return DockManager.getDefault().iconTheme; + } + + get settings() { // eslint-disable-line no-dupe-class-members + return this._settings; + } + + get iconTheme() { + return this._iconTheme; + } + + get fm1Client() { + return this._fm1Client; + } + + get remoteModel() { + return this._remoteModel; + } + + get mainDock() { + return this._allDocks.length ? this._allDocks[0] : null; + } + + get removables() { + return this._removables; + } + + get trash() { + return this._trash; + } + + get desktopIconsUsableArea() { + return this._desktopIconsUsableArea; + } + + get discreteGpuAvailable() { + return AppDisplay.discreteGpuAvailable || this._discreteGpuAvailable; + } + + get appSpread() { + return this._appSpread; + } + + get notificationsMonitor() { + return this._notificationsMonitor; + } + + getDockByMonitor(monitorIndex) { + return this._allDocks.find(d => d.monitorIndex === monitorIndex); + } + + _ensureLocations() { + const {showMounts, showTrash} = this.settings; + + if (showTrash || showMounts) { + if (!this._fm1Client) + this._fm1Client = new FileManager1API.FileManager1Client(); + } else if (this._fm1Client) { + this._fm1Client.destroy(); + this._fm1Client = null; + } + + if (showMounts && !this._removables) { + this._removables = new Locations.Removables(); + } else if (!showMounts && this._removables) { + this._removables.destroy(); + this._removables = null; + } + + if (showTrash && !this._trash) { + this._trash = new Locations.Trash(); + } else if (!showTrash && this._trash) { + this._trash.destroy(); + this._trash = null; + } + + Locations.unWrapFileManagerApp(); + [this._methodInjections, this._propertyInjections].forEach( + injections => injections.removeWithLabel(Labels.LOCATIONS)); + + if (showMounts || showTrash) { + if (this.settings.isolateLocations) { + const fileManagerApp = Locations.wrapFileManagerApp(); + + this._methodInjections.addWithLabel(Labels.LOCATIONS, [ + Shell.AppSystem.prototype, 'get_running', + function (originalMethod, ...args) { + /* eslint-disable no-invalid-this */ + const runningApps = originalMethod.call(this, ...args); + const locationApps = Locations.getRunningApps(); + if (!locationApps.length) + return runningApps; + + const fileManagerIdx = runningApps.indexOf(fileManagerApp); + if (fileManagerIdx > -1 && fileManagerApp?.state !== Shell.AppState.RUNNING) + runningApps.splice(fileManagerIdx, 1); + + return [...runningApps, ...locationApps].sort(Utils.shellAppCompare); + /* eslint-enable no-invalid-this */ + }, + ], + [ + Shell.WindowTracker.prototype, 'get_window_app', + function (originalMethod, window) { + /* eslint-disable no-invalid-this */ + const locationApp = Locations.getRunningApps().find(a => + a.get_windows().includes(window)); + return locationApp ?? originalMethod.call(this, window); + /* eslint-enable no-invalid-this */ + }, + ], + [ + Shell.WindowTracker.prototype, 'get_app_from_pid', + function (originalMethod, pid) { + /* eslint-disable no-invalid-this */ + const locationApp = Locations.getRunningApps().find(a => + a.get_pids().includes(pid)); + return locationApp ?? originalMethod.call(this, pid); + /* eslint-enable no-invalid-this */ + }, + ]); + + const {get: defaultFocusAppGetter} = Object.getOwnPropertyDescriptor( + Shell.WindowTracker.prototype, 'focus_app'); + this._propertyInjections.addWithLabel(Labels.LOCATIONS, + Shell.WindowTracker.prototype, 'focus_app', { + get() { + const locationApp = Locations.getRunningApps().find(a => a.isFocused); + return locationApp ?? defaultFocusAppGetter.call(this); + }, + }); + } + } + } + + _toggle() { + if (this._toggleLater) + return; + + this._toggleLater = Utils.laterAdd(Meta.LaterType.BEFORE_REDRAW, () => { + delete this._toggleLater; + this._restoreDash(); + this._deleteDocks(); + this._createDocks(); + this.emit('toggled'); + }); + } + + _mapExternalSetting(settings, key, mappedKey, mapValueFunction) { + const camelMappedKey = mappedKey.replace(/-([a-z\d])/g, k => k[1].toUpperCase()); + + const dockPropertyDesc = Object.getOwnPropertyDescriptor(this.settings, camelMappedKey); + + if (!dockPropertyDesc) + throw new Error('Setting %s not found in dock'.format(mappedKey)); + + const mappedValue = () => mapValueFunction(settings.get_value(key).recursiveUnpack()); + Object.defineProperty(this.settings, camelMappedKey, { + get: () => mappedValue() ?? dockPropertyDesc.value, + set: value => { + if (mappedValue()) + dockPropertyDesc.value = value; + }, + }); + + this._signalsHandler.addWithLabel(Labels.SETTINGS, settings, + 'changed::%s'.format(key), () => { + this._signalsHandler.blockWithLabel(Labels.SETTINGS); + this.settings.emit('changed::%s'.format(mappedKey), mappedKey); + this._signalsHandler.unblockWithLabel(Labels.SETTINGS); + }); + } + + _mapSettingsValues() { + this.settings.settingsSchema.list_keys().forEach(key => { + const camelKey = key.replace(/-([a-z\d])/g, k => k[1].toUpperCase()); + const updateSetting = () => { + const schemaKey = this.settings.settingsSchema.get_key(key); + if (schemaKey.get_range().deepUnpack()[0] === 'enum') + this.settings[camelKey] = this.settings.get_enum(key); + else + this.settings[camelKey] = this.settings.get_value(key).recursiveUnpack(); + }; + updateSetting(); + this._signalsHandler.addWithLabel(Labels.SETTINGS, this.settings, + `changed::${key}`, updateSetting); + if (key !== camelKey) { + Object.defineProperty(this.settings, key, + {get: () => this.settings[camelKey]}); + } + }); + Object.defineProperties(this.settings, { + dockExtended: {get: () => this.settings.extendHeight}, + }); + } + + _bindSettingsChanges() { + // Connect relevant signals to the toggling function + this._signalsHandler.addWithLabel(Labels.SETTINGS, [ + Utils.getMonitorManager(), + 'monitors-changed', + this._toggle.bind(this), + ], [ + Main.sessionMode, + 'updated', + this._toggle.bind(this), + ], [ + this._settings, + 'changed::multi-monitor', + this._toggle.bind(this), + ], [ + this._settings, + 'changed::preferred-monitor', + this._toggle.bind(this), + ], [ + this._settings, + 'changed::preferred-monitor-by-connector', + this._toggle.bind(this), + ], [ + this._settings, + 'changed::dock-position', + this._toggle.bind(this), + ], [ + this._settings, + 'changed::extend-height', + () => this._adjustPanelCorners(), + ], [ + this._settings, + 'changed::dock-fixed', + () => this._adjustPanelCorners(), + ], [ + this._settings, + 'changed::show-trash', + () => this._ensureLocations(), + ], [ + this._settings, + 'changed::show-mounts', + () => this._ensureLocations(), + ], [ + this._settings, + 'changed::isolate-locations', + () => this._ensureLocations(), + ], [ + this._settings, + 'changed::intellihide', + () => { + if (!this._settings.intellihide) + this._desktopIconsUsableArea.resetMargins(); + }, + ]); + + this._mapExternalSetting(this._appSwitcherSettings, 'current-workspace-only', + 'isolate-workspaces', value => value || undefined); + } + + _createDocks() { + // If there are no monitors (headless configurations, but it can also + // happen temporary while disconnecting and reconnecting monitors), just + // do nothing. When a monitor will be connected we we'll be notified and + // and thus create the docks. This prevents pointing trying to access + // monitors throughout the code, were we are assuming that at least the + // primary monitor is present. + if (Main.layoutManager.monitors.length <= 0) + return; + + + this._preferredMonitorIndex = this.settings.preferredMonitor; + if (this._preferredMonitorIndex === -2) { + const monitorManager = Utils.getMonitorManager(); + this._preferredMonitorIndex = monitorManager.get_monitor_for_connector( + this.settings.preferredMonitorByConnector); + } else if (this._preferredMonitorIndex >= 0) { + // Primary monitor used to be always 0 in Gdk, but the shell has a different + // concept (where the order depends on mutter order). + // So even if now the extension settings may use the same logic of the shell + // we prefer not to break the previously configured systems, and so we still + // assume that the gsettings monitor numbering follows the old strategy. + // This ensure the indexing in the settings and in the shell are matched, + // i.e. that we start counting from the primaryMonitorIndex + this._preferredMonitorIndex = + (Main.layoutManager.primaryIndex + this._preferredMonitorIndex) % + Main.layoutManager.monitors.length; + } + + // In case of multi-monitor, we consider the dock on the primary monitor + // to be the preferred (main) one regardless of the settings the dock + // goes on the primary monitor also if the settings are inconsistent + // (e.g. desired monitor not connected). + if (this.settings.multiMonitor || + this._preferredMonitorIndex < 0 || + this._preferredMonitorIndex > Main.layoutManager.monitors.length - 1) + this._preferredMonitorIndex = Main.layoutManager.primaryIndex; + + + // First we create the main Dock, to get the extra features to bind to this one + let dock = new DockedDash({ + monitorIndex: this._preferredMonitorIndex, + isMain: true, + }); + this._allDocks.push(dock); + + // connect app icon into the view selector + dock.dash.showAppsButton.connect('notify::checked', + this._onShowAppsButtonToggled.bind(this)); + + // Make the necessary changes to Main.overview.dash + this._prepareMainDash(); + + // Adjust corners if necessary + this._adjustPanelCorners(); + + if (this.settings.multiMonitor) { + const nMon = Main.layoutManager.monitors.length; + for (let iMon = 0; iMon < nMon; iMon++) { + if (iMon === this._preferredMonitorIndex) + continue; + dock = new DockedDash({monitorIndex: iMon}); + this._allDocks.push(dock); + // connect app icon into the view selector + dock.dash.showAppsButton.connect('notify::checked', + this._onShowAppsButtonToggled.bind(this)); + } + } + + // Load optional features. We load *after* the docks are created, since + // we need to connect the signals to all dock instances. + this._workspaceIsolation = new WorkspaceIsolation(); + this._keyboardShortcuts = new KeyboardShortcuts(); + + this.emit('docks-ready'); + } + + _prepareStartupAnimation() { + DockManager.allDocks.forEach(dock => { + const {dash} = dock; + + dock.opacity = 255; + dash.set({ + opacity: 0, + translation_x: 0, + translation_y: 0, + }); + }); + } + + _runStartupAnimation() { + DockManager.allDocks.forEach(dock => { + const {dash} = dock; + + switch (dock.position) { + case St.Side.LEFT: + dash.translation_x = -dash.width; + break; + case St.Side.RIGHT: + dash.translation_x = dash.width; + break; + case St.Side.BOTTOM: + dash.translation_y = dash.height; + break; + case St.Side.TOP: + dash.translation_y = -dash.height; + break; + } + + dash.ease({ + opacity: 255, + translation_x: 0, + translation_y: 0, + duration: STARTUP_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + }); + } + + _prepareMainDash() { + // Ensure Main.overview.dash is set to our dash in dummy mode + // while just use the default getter otherwise. + // The getter must be dynamic and not set only when we've a dummy + // overview because the mode can change dynamically. + this._propertyInjections.removeWithLabel(Labels.MAIN_DASH); + const defaultDashGetter = Object.getOwnPropertyDescriptor( + Main.overview.constructor.prototype, 'dash').get; + this._propertyInjections.addWithLabel(Labels.MAIN_DASH, Main.overview, 'dash', { + get: () => Main.overview.isDummy + ? this.mainDock.dash : defaultDashGetter.call(Main.overview), + }); + + if (Main.overview.isDummy) + return; + + // Hide usual Dash + this._oldDash.hide(); + + // Also set dash width to 1, so it's almost not taken into account by code + // calculating the reserved space in the overview. The reason to keep it at 1 is + // to allow its visibility change to trigger an allocation of the appGrid which + // in turn is triggering the appsIcon spring animation, required when no other + // actors has this effect, i.e in horizontal mode and without the workspaceThumbnails + // 1 static workspace only) + this._oldDash.set_height(1); + + this._signalsHandler.addWithLabel(Labels.OLD_DASH_CHANGES, [ + this._oldDash, + 'notify::visible', + () => this._oldDash.hide(), + ], [ + this._oldDash, + 'notify::height', + () => this._oldDash.set_height(1), + ]); + + // Pretend I'm the dash: meant to make app grid swarm animation come from + // the right position of the appShowButton. + this.overviewControls.dash = this.mainDock.dash; + this.searchController._showAppsButton = this.mainDock.dash.showAppsButton; + + // We also need to ignore max-size changes + this._methodInjections.addWithLabel(Labels.MAIN_DASH, this._oldDash, + 'setMaxSize', () => {}); + this._methodInjections.addWithLabel(Labels.MAIN_DASH, this._oldDash, + 'allocate', () => {}); + // And to return the preferred height depending on the state + this._methodInjections.addWithLabel(Labels.MAIN_DASH, this._oldDash, + 'get_preferred_height', (_originalMethod, ...args) => { + if (this.mainDock.isHorizontal && !this.settings.dockFixed) + return this.mainDock.get_preferred_height(...args); + return [0, 0]; + }); + + // FIXME: https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2890 + // const { ControlsManagerLayout } = OverviewControls; + const ControlsManagerLayout = this.overviewControls.layout_manager.constructor; + + const maybeAdjustBoxSize = (state, box, spacing) => { + // ensure that an undefined value will be converted into a valid one + spacing = spacing ?? 0; + + if (state === OverviewControls.ControlsState.WINDOW_PICKER) { + const searchBox = this.overviewControls._searchEntry.get_allocation_box(); + const {shouldShow: wsThumbnails} = this.overviewControls._thumbnailsBox; + + if (!wsThumbnails) { + box.y1 += spacing; + box.y2 -= spacing; + } + + box.y2 -= searchBox.get_height() + 2 * spacing; + } + + return box; + }; + + const maybeAdjustBoxToDock = (state, box, spacing) => { + maybeAdjustBoxSize(state, box, spacing); + + if (this.mainDock.isHorizontal || this.settings.dockFixed) + return box; + + const [, preferredWidth] = this.mainDock.get_preferred_width( + box.get_height()); + + if (this.mainDock.position === St.Side.LEFT) + box.x1 += preferredWidth; + else if (this.mainDock.position === St.Side.RIGHT) + box.x2 -= preferredWidth; + + return box; + }; + + this._vfuncInjections.addWithLabel(Labels.MAIN_DASH, ControlsManagerLayout.prototype, + 'allocate', function (container) { + /* eslint-disable no-invalid-this */ + const oldPostAllocation = this._runPostAllocation; + this._runPostAllocation = () => {}; + + const monitor = Main.layoutManager.findMonitorForActor(container); + const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index); + const startX = workArea.x - monitor.x; + const startY = workArea.y - monitor.y; + const workAreaBox = new Clutter.ActorBox(); + workAreaBox.set_origin(startX, startY); + workAreaBox.set_size(workArea.width, workArea.height); + + // GNOME 46 changes "spacing" to "_spacing". + const spacing = this.spacing ?? this._spacing; + + maybeAdjustBoxToDock(undefined, workAreaBox, spacing); + const oldStartY = workAreaBox.y1; + + const propertyInjections = new Utils.PropertyInjectionsHandler(); + propertyInjections.add(Main.layoutManager.panelBox, 'height', {value: startY}); + + if (Main.layoutManager.panelBox.y === Main.layoutManager.primaryMonitor.y) + workAreaBox.y1 -= oldStartY; + + this.vfunc_allocate(container, workAreaBox); + + propertyInjections.destroy(); + workAreaBox.y1 = oldStartY; + + const adjustActorHorizontalAllocation = actor => { + if (!actor.visible || !workAreaBox.x1) + return; + + const contentBox = actor.get_allocation_box(); + contentBox.set_size(workAreaBox.get_width(), contentBox.get_height()); + contentBox.set_origin(workAreaBox.x1, contentBox.y1); + actor.allocate(contentBox); + }; + + [this._searchEntry, this._workspacesThumbnails, this._searchController].forEach( + actor => adjustActorHorizontalAllocation(actor)); + + this._runPostAllocation = oldPostAllocation; + this._runPostAllocation(); + /* eslint-enable no-invalid-this */ + }); + + /** + * This can be removed or bypassed when GNOME/gnome-shell!1892 will be merged + * + * @param originalFunction + * @param state + * @param workAreaBox + * @param {...any} args + */ + function workspaceBoxOriginFixer(originalFunction, state, workAreaBox, ...args) { + /* eslint-disable no-invalid-this */ + const workspaceBox = originalFunction.call(this, state, workAreaBox, ...args); + workspaceBox.set_origin(workAreaBox.x1, workspaceBox.y1); + return workspaceBox; + /* eslint-enable no-invalid-this */ + } + + this._methodInjections.addWithLabel(Labels.MAIN_DASH, [ + ControlsManagerLayout.prototype, + '_computeWorkspacesBoxForState', + function (originalFunction, state, ...args) { + /* eslint-disable no-invalid-this */ + if (state === OverviewControls.ControlsState.HIDDEN) + return originalFunction.call(this, state, ...args); + + const box = workspaceBoxOriginFixer.call(this, originalFunction, state, ...args); + // GNOME 46 changes "spacing" to "_spacing". + const spacing = this.spacing ?? this._spacing; + const dock = DockManager.getDefault().getDockByMonitor(Main.layoutManager.primaryIndex); + if (!dock) + return box; + else + return maybeAdjustBoxSize(state, box, spacing); + /* eslint-enable no-invalid-this */ + }, + ], [ + WorkspacesView.SecondaryMonitorDisplay.prototype, + '_getWorkspacesBoxForState', + function (originalFunction, state, ...args) { + /* eslint-disable no-invalid-this */ + if (state === OverviewControls.ControlsState.HIDDEN) + return originalFunction.call(this, state, ...args); + + const box = workspaceBoxOriginFixer.call(this, originalFunction, state, ...args); + const dock = DockManager.getDefault().getDockByMonitor(this._monitorIndex); + if (!dock) + return box; + if (state === OverviewControls.ControlsState.WINDOW_PICKER && + dock.position === St.Side.BOTTOM) { + const [, preferredHeight] = dock.get_preferred_height(box.get_width()); + box.y2 -= preferredHeight; + } + return box; + /* eslint-enable no-invalid-this */ + }, + ], [ + ControlsManagerLayout.prototype, + '_getAppDisplayBoxForState', + function (originalFunction, ...args) { + /* eslint-disable no-invalid-this */ + return workspaceBoxOriginFixer.call(this, originalFunction, ...args); + /* eslint-enable no-invalid-this */ + }, + ]); + + this._vfuncInjections.addWithLabel(Labels.MAIN_DASH, Workspace.WorkspaceBackground.prototype, + 'allocate', function (box) { + /* eslint-disable no-invalid-this */ + this.vfunc_allocate(box); + + // This code has been submitted upstream via GNOME/gnome-shell!1892 + // so can be removed when that gets merged (or bypassed on newer shell + // versions). + const monitor = Main.layoutManager.monitors[this._monitorIndex]; + const [contentWidth, contentHeight] = this._bin.get_content_box().get_size(); + const [mX1, mX2] = [monitor.x, monitor.x + monitor.width]; + const [mY1, mY2] = [monitor.y, monitor.y + monitor.height]; + const [wX1, wX2] = [this._workarea.x, this._workarea.x + this._workarea.width]; + const [wY1, wY2] = [this._workarea.y, this._workarea.y + this._workarea.height]; + const xScale = contentWidth / this._workarea.width; + const yScale = contentHeight / this._workarea.height; + const leftOffset = wX1 - mX1; + const topOffset = wY1 - mY1; + const rightOffset = mX2 - wX2; + const bottomOffset = mY2 - wY2; + + const contentBox = new Clutter.ActorBox(); + contentBox.set_origin(-leftOffset * xScale, -topOffset * yScale); + contentBox.set_size( + contentWidth + (leftOffset + rightOffset) * xScale, + contentHeight + (topOffset + bottomOffset) * yScale); + + this._backgroundGroup.allocate(contentBox); + /* eslint-enable no-invalid-this */ + }); + + // Reduce the space that the workspaces can use in secondary monitors + this._methodInjections.addWithLabel(Labels.MAIN_DASH, WorkspacesView.WorkspacesView.prototype, + '_getFirstFitAllWorkspaceBox', function (originalFunction, ...args) { + /* eslint-disable no-invalid-this */ + const box = originalFunction.call(this, ...args); + if (DockManager.settings.dockFixed || + this._monitorIndex === Main.layoutManager.primaryIndex) + return box; + + const dock = DockManager.getDefault().getDockByMonitor(this._monitorIndex); + if (!dock) + return box; + + if (dock.isHorizontal) { + const [, preferredHeight] = dock.get_preferred_height(box.get_width()); + box.y2 -= preferredHeight; + if (dock.position === St.Side.TOP) + box.set_origin(box.x1, box.y1 + preferredHeight); + } else { + const [, preferredWidth] = dock.get_preferred_width(box.get_height()); + box.x2 -= preferredWidth / 2; + if (dock.position === St.Side.LEFT) + box.set_origin(box.x1 + preferredWidth, box.y1); + } + return box; + /* eslint-enable no-invalid-this */ + }); + + if (AppDisplay.BaseAppView?.prototype?._pageForCoords) { + // Ensure we handle Dnd events happening on the dock when we're + // dragging from AppDisplay. + // Remove when merged + // https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2002 + this._methodInjections.addWithLabel(Labels.MAIN_DASH, + AppDisplay.BaseAppView.prototype, + '_pageForCoords', function (originalFunction, ...args) { + /* eslint-disable no-invalid-this */ + if (!this._scrollView.has_pointer) + return AppDisplay.SidePages.NONE; + return originalFunction.call(this, ...args); + /* eslint-enable no-invalid-this */ + }); + } + + if (Main.layoutManager._startingUp) { + this._prepareStartupAnimation(); + + const hadOverview = Main.sessionMode.hasOverview; + + // Convince LayoutManager to use the legacy startup animation: + // Reset overview controls state to HIDDEN, as skipping the startup + // overview leaves it stuck at WINDOW_PICKER + if (this._settings.disableOverviewOnStartup) { + Main.sessionMode.hasOverview = false; + Main.overview._overview.controls._stateAdjustment.value = + OverviewControls.ControlsState.HIDDEN; + } + + this._signalsHandler.addWithLabel(Labels.STARTUP_ANIMATION, + Main.layoutManager, 'startup-complete', () => { + this._signalsHandler.removeWithLabel(Labels.STARTUP_ANIMATION); + Main.sessionMode.hasOverview = hadOverview; + this._runStartupAnimation(); + }); + } + } + + _deleteDocks() { + if (!this._allDocks.length) + return; + + // Remove extra features + this._workspaceIsolation.destroy(); + this._keyboardShortcuts.destroy(); + this._desktopIconsUsableArea.resetMargins(); + + // Delete all docks + this._allDocks.forEach(d => d.destroy()); + this._allDocks = []; + + this.emit('docks-destroyed'); + } + + _restoreDash() { + if (!this._oldDash) + return; + + this._signalsHandler.removeWithLabel(Labels.OLD_DASH_CHANGES); + [this._methodInjections, this._vfuncInjections, this._propertyInjections].forEach( + injections => injections.removeWithLabel(Labels.MAIN_DASH)); + + this.overviewControls.layout_manager._dash = this._oldDash; + this.overviewControls.dash = this._oldDash; + this.searchController._showAppsButton = this._oldDash.showAppsButton; + Main.overview.dash.show(); + Main.overview.dash.set_height(-1); // reset default dash size + // This force the recalculation of the icon size + Main.overview.dash._maxHeight = -1; + } + + get overviewControls() { + return Main.overview._overview.controls; + } + + get searchController() { + return this.overviewControls._searchController; + } + + _onShowAppsButtonToggled(button) { + const {checked} = button; + const {overviewControls} = this; + + if (!Main.overview.visible) { + this.mainDock.dash.showAppsButton._fromDesktop = true; + Main.overview.show(OverviewControls.ControlsState.APP_GRID); + } else if (!checked && this.mainDock.dash.showAppsButton._fromDesktop) { + Main.overview.hide(); + this.mainDock.dash.showAppsButton._fromDesktop = false; + } else { + // TODO: I'm not sure how reliable this is, we might need to move the + // _onShowAppsButtonToggled logic into the extension. + if (!checked) + this.mainDock.dash.showAppsButton._fromDesktop = false; + + + // Instead of "syncing" the stock button, let's call its callback directly. + overviewControls._onShowAppsButtonToggled(); + } + + // Because we "disconnected" from the search controller, we have to manage its state. + this.searchController._setSearchActive(false); + } + + _overrideAppMenus() { + this._methodInjections.add(AppMenu.AppMenu.prototype, + '_updateFavoriteItem', function (originalFunction, ...args) { + /* eslint-disable no-invalid-this */ + originalFunction.call(this, ...args); + if (!this._toggleFavoriteItem.visible) + return; + + const {id} = this._app; + this._toggleFavoriteItem.label.text = this._appFavorites.isFavorite(id) + ? _('Unpin') : __('Pin to Dock'); + /* eslint-enable no-invalid-this */ + }); + } + + destroy() { + this.emit('destroy'); + if (this._toggleLater) { + Utils.laterRemove(this._toggleLater); + delete this._toggleLater; + } + this._restoreDash(); + this._deleteDocks(); + this._revertPanelCorners(); + if (this._oldSelectorMargin) + this.searchController.margin_bottom = this._oldSelectorMargin; + if (this._fm1Client) { + this._fm1Client.destroy(); + this._fm1Client = null; + } + this._notificationsMonitor.destroy(); + this._appSpread.destroy(); + this._trash?.destroy(); + this._trash = null; + Locations.unWrapFileManagerApp(); + this._removables?.destroy(); + this._removables = null; + this._iconTheme = null; + this._remoteModel?.destroy(); + this._appIconsDecorator?.destroy(); + this._settings = null; + this._appSwitcherSettings = null; + this._oldDash = null; + + this._desktopIconsUsableArea?.destroy(); + this._desktopIconsUsableArea = null; + this._extension = null; + DockManager._singleton = null; + } + + /** + * Adjust Panel corners, remove this when 41 won't be supported anymore + */ + _adjustPanelCorners() { + if (!this._hasPanelCorners()) + return; + + const position = Utils.getPosition(); + const isHorizontal = (position === St.Side.TOP) || (position === St.Side.BOTTOM); + const dockOnPrimary = this._settings.multiMonitor || + this._preferredMonitorIndex === Main.layoutManager.primaryIndex; + + if (!isHorizontal && dockOnPrimary && this.settings.dockExtended && this.settings.dockFixed) { + Main.panel._rightCorner.hide(); + Main.panel._leftCorner.hide(); + } else { + this._revertPanelCorners(); + } + } + + _revertPanelCorners() { + if (!this._hasPanelCorners()) + return; + + Main.panel._leftCorner.show(); + Main.panel._rightCorner.show(); + } + + _hasPanelCorners() { + return !!Main.panel?._rightCorner && !!Main.panel?._leftCorner; + } +} +Signals.addSignalMethods(DockManager.prototype); + +// This class drives long-running icon animations, to keep them running in sync +// with each other, and to save CPU by pausing them when the dock is hidden. +export class IconAnimator { + constructor(actor) { + this._count = 0; + this._started = false; + this._animations = { + wiggle: [], + }; + this._timeline = new Clutter.Timeline({ + duration: AnimationUtils.adjustAnimationTime(ICON_ANIMATOR_DURATION) || 1, + repeat_count: -1, + actor, + }); + + this._updateSettings(); + this._settingsChangedId = St.Settings.get().connect('notify', + () => this._updateSettings()); + + this._timeline.connect('new-frame', () => { + const progress = this._timeline.get_progress(); + const wiggleRotation = progress < 1 / 6 ? 15 * Math.sin(progress * 24 * Math.PI) : 0; + const wigglers = this._animations.wiggle; + for (let i = 0, iMax = wigglers.length; i < iMax; i++) + wigglers[i].target.rotation_angle_z = wiggleRotation; + }); + } + + _updateSettings() { + this._timeline.set_duration( + AnimationUtils.adjustAnimationTime(ICON_ANIMATOR_DURATION) || 1); + } + + destroy() { + St.Settings.get().disconnect(this._settingsChangedId); + this._timeline.stop(); + this._timeline = null; + for (const pairs of Object.values(this._animations)) { + for (let i = 0, iMax = pairs.length; i < iMax; i++) { + const pair = pairs[i]; + pair.target.disconnect(pair.targetDestroyId); + } + } + this._animations = null; + } + + pause() { + if (this._started && this._count > 0) + this._timeline.stop(); + + this._started = false; + } + + start() { + if (!this._started && this._count > 0) + this._timeline.start(); + + this._started = true; + } + + addAnimation(target, name) { + const targetDestroyId = target.connect('destroy', + () => this.removeAnimation(target, name)); + this._animations[name].push({target, targetDestroyId}); + if (this._started && this._count === 0) + this._timeline.start(); + + this._count++; + } + + removeAnimation(target, name) { + const pairs = this._animations[name]; + for (let i = 0, iMax = pairs.length; i < iMax; i++) { + const pair = pairs[i]; + if (pair.target === target) { + target.disconnect(pair.targetDestroyId); + pairs.splice(i, 1); + this._count--; + if (this._started && this._count === 0) + this._timeline.stop(); + + return; + } + } + } +} |