%PDF- %PDF-
Direktori : /usr/share/gnome-shell/extensions/ubuntu-dock@ubuntu.com/ |
Current File : //usr/share/gnome-shell/extensions/ubuntu-dock@ubuntu.com/docking.js |
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- import { Clutter, GLib, Gio, GObject, Meta, Shell, St, } from './dependencies/gi.js'; import { AppDisplay, Layout, Main, Overview, OverviewControls, PointerWatcher, Workspace, WorkspacesView, WorkspaceSwitcherPopup, } from './dependencies/shell/ui.js'; import { AnimationUtils, } from './dependencies/shell/misc.js'; import { AppSpread, DockDash, DesktopIconsIntegration, FileManager1API, Intellihide, LauncherAPI, Locations, NotificationsMonitor, Theming, Utils, } from './imports.js'; 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'), 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 overlows the parent actor. By doing * this the region of the dash that is slideout is not steling anymore the input * regions making the extesion 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.monitorİndex // 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 Meta.disable_unredirect_for_display() 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) { Main.layoutManager.addChrome(this, { trackFullscreen: true, affectsStruts: true, }); } else { Main.layoutManager.addChrome(this); } } _initialize() { this._signalsHandler.removeWithLabel(Labels.INITIALIZE); // Apply custome 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(); }, ]); } _restoreUnredirect() { if (this._unredirectDisabled) { Meta.enable_unredirect_for_display(global.display); 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._unredirectDisabled && this._intellihideIsEnabled) { Meta.disable_unredirect_for_display(global.display); this._unredirectDisabled = true; } 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._unredirectDisabled) { Meta.enable_unredirect_for_display(global.display); this._unredirectDisabled = false; } // Remove queued barried removal 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(); } /** * 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 deadtime between each scroll event. // Usefull on laptops when using a touchpad. // 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 behaviour. 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(direction, 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 keybaord 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 implementaion 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; if (needsRemoteModel) this._remoteModel = new LauncherAPI.LauncherEntryRemoteModel(); const ensureRemoteModel = () => { if (needsRemoteModel && !this._remoteModel) { this._remoteModel = new LauncherAPI.LauncherEntryRemoteModel(); } else if (!needsRemoteModel && this._remoteModel) { this._remoteModel.destroy(); delete this._remoteModel; } }; 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(); // 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 // calculaing the reserved space in the overview. The reason to keep it at 1 is // to allow its visibility change to trigger an allocaion of the appGrid which // in turn is triggergin the appsIcon spring animation, required when no other // actors has this effect, i.e in horizontal mode and without the workspaceThumnails // 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 appgrid 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(this._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: if (this._settings.disableOverviewOnStartup) Main.sessionMode.hasOverview = false; const id = Main.layoutManager.connect('startup-complete', () => { Main.sessionMode.hasOverview = hadOverview; Main.layoutManager.disconnect(id); 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; if (this._settings.animateShowApps) { Main.overview.show(OverviewControls.ControlsState.APP_GRID); } else { GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { const oldAnimationTime = OverviewControls.SIDE_CONTROLS_ANIMATION_TIME; Overview.ANIMATION_TIME = 1; const id = Main.overview.connect('shown', () => { Overview.ANIMATION_TIME = oldAnimationTime; Main.overview.disconnect(id); }); Main.overview.show(OverviewControls.ControlsState.APP_GRID); return GLib.SOURCE_REMOVE; }); } } else if (!checked && this.mainDock.dash.showAppsButton._fromDesktop) { if (this._settings.animateShowApps) { Main.overview.hide(); this.mainDock.dash.showAppsButton._fromDesktop = false; } else { GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { const oldAnimationTime = Overview.ANIMATION_TIME; Overview.ANIMATION_TIME = 1; const id = Main.overview.connect('hidden', () => { Overview.ANIMATION_TIME = oldAnimationTime; Main.overview.disconnect(id); }); Main.overview.hide(); this.mainDock.dash.showAppsButton._fromDesktop = false; return GLib.SOURCE_REMOVE; }); } } 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); } 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._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; } } } }