%PDF- %PDF-
Direktori : /usr/share/gnome-shell/extensions/ubuntu-dock@ubuntu.com/ |
Current File : //usr/share/gnome-shell/extensions/ubuntu-dock@ubuntu.com/dash.js |
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- import { Clutter, Gio, GLib, GObject, Shell, St, } from './dependencies/gi.js'; import { AppFavorites, Dash, DND, Main, } from './dependencies/shell/ui.js'; import { Util, } from './dependencies/shell/misc.js'; import { AppIcons, Docking, Theming, Utils, } from './imports.js'; // module "Dash" does not export DASH_ANIMATION_TIME // so we just define it like it is defined in Dash; // taken from https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/dash.js const DASH_ANIMATION_TIME = 200; const DASH_VISIBILITY_TIMEOUT = 3; const Labels = Object.freeze({ SHOW_MOUNTS: Symbol('show-mounts'), FIRST_LAST_CHILD_WORKAROUND: Symbol('first-last-child-workaround'), }); /** * Extend DashItemContainer * * - set label position based on dash orientation * */ const DockDashItemContainer = GObject.registerClass( class DockDashItemContainer extends Dash.DashItemContainer { _init(position) { super._init(); this.label?.add_style_class_name(Theming.PositionStyleClass[position]); if (Docking.DockManager.settings.customThemeShrink) this.label?.add_style_class_name('shrink'); } showLabel() { return AppIcons.itemShowLabel.call(this); } // we override the method show taken from: // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/dash.js // in order to apply a little modification at the end of the animation // which makes sure that the icon background is not blurry show(animate) { if (this.child == null) return; this.ease({ scale_x: 1, scale_y: 1, opacity: 255, duration: animate ? DASH_ANIMATION_TIME : 0, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { // when the animation is ended, we simulate // a hover to gain back focus and unblur the // background this.set_hover(true); }, }); } }); const DockDashIconsVerticalLayout = GObject.registerClass( class DockDashIconsVerticalLayout extends Clutter.BoxLayout { _init() { super._init({ orientation: Clutter.Orientation.VERTICAL, }); } vfunc_get_preferred_height(container, forWidth) { const [natHeight] = super.vfunc_get_preferred_height(container, forWidth); return [natHeight, 0]; } }); const baseIconSizes = [16, 22, 24, 32, 48, 64, 96, 128]; /** * This class is a fork of the upstream dash class (ui.dash.js) * * Summary of changes: * - disconnect global signals adding a destroy method; * - play animations even when not in overview mode * - set a maximum icon size * - show running and/or favorite applications * - hide showApps label when the custom menu is shown. * - add scrollview * ensure actor is visible on keyfocus inseid the scrollview * - add 128px icon size, might be useful for hidpi display * - sync minimization application target position. * - keep running apps ordered. */ export const DockDash = GObject.registerClass({ Properties: { 'requires-visibility': GObject.ParamSpec.boolean( 'requires-visibility', 'requires-visibility', 'requires-visibility', GObject.ParamFlags.READWRITE, false), }, Signals: { 'menu-opened': {}, 'menu-closed': {}, 'icon-size-changed': {}, }, }, class DockDash extends St.Widget { _init(monitorIndex) { // Initialize icon variables and size super._init({ name: 'dash', offscreen_redirect: Clutter.OffscreenRedirect.ALWAYS, layout_manager: new Clutter.BinLayout(), }); this._maxWidth = -1; this._maxHeight = -1; this.iconSize = Docking.DockManager.settings.dashMaxIconSize; this._availableIconSizes = baseIconSizes; this._shownInitially = false; this._initializeIconSize(this.iconSize); this._signalsHandler = new Utils.GlobalSignalsHandler(this); this._separator = null; this._monitorIndex = monitorIndex; this._position = Utils.getPosition(); this._isHorizontal = (this._position === St.Side.TOP) || (this._position === St.Side.BOTTOM); this._dragPlaceholder = null; this._dragPlaceholderPos = -1; this._animatingPlaceholdersCount = 0; this._showLabelTimeoutId = 0; this._resetHoverTimeoutId = 0; this._labelShowing = false; this._dashContainer = new St.BoxLayout({ name: 'dashtodockDashContainer', x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER, vertical: !this._isHorizontal, y_expand: this._isHorizontal, x_expand: !this._isHorizontal, }); this._scrollView = new St.ScrollView({ name: 'dashtodockDashScrollview', hscrollbar_policy: this._isHorizontal ? St.PolicyType.EXTERNAL : St.PolicyType.NEVER, vscrollbar_policy: this._isHorizontal ? St.PolicyType.NEVER : St.PolicyType.EXTERNAL, x_expand: this._isHorizontal, y_expand: !this._isHorizontal, enable_mouse_scrolling: false, }); this._scrollView.connect('scroll-event', this._onScrollEvent.bind(this)); this._boxContainer = new St.BoxLayout({ name: 'dashtodockBoxContainer', x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.FILL, vertical: !this._isHorizontal, }); this._boxContainer.add_style_class_name(Theming.PositionStyleClass[this._position]); const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL; this._box = new St.BoxLayout({ vertical: !this._isHorizontal, clip_to_allocation: false, ...!this._isHorizontal ? {layout_manager: new DockDashIconsVerticalLayout()} : {}, x_align: rtl ? Clutter.ActorAlign.END : Clutter.ActorAlign.START, y_align: this._isHorizontal ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START, y_expand: !this._isHorizontal, x_expand: this._isHorizontal, }); this._box._delegate = this; this._boxContainer.add_child(this._box); if (this._scrollView.add_actor) this._scrollView.add_actor(this._boxContainer); else this._scrollView.add_child(this._boxContainer); this._dashContainer.add_child(this._scrollView); this._showAppsIcon = new AppIcons.DockShowAppsIcon(this._position); this._showAppsIcon.show(false); this._showAppsIcon.icon.setIconSize(this.iconSize); this._showAppsIcon.x_expand = false; this._showAppsIcon.y_expand = false; this.showAppsButton.connect('notify::hover', a => { if (this._showAppsIcon.get_parent() === this._boxContainer) this._ensureItemVisibility(a); }); if (!this._isHorizontal) this._showAppsIcon.y_align = Clutter.ActorAlign.START; this._hookUpLabel(this._showAppsIcon); this._showAppsIcon.connect('menu-state-changed', (_icon, opened) => { this._itemMenuStateChanged(this._showAppsIcon, opened); }); this.updateShowAppsButton(); this._background = new St.Widget({ style_class: 'dash-background', y_expand: this._isHorizontal, x_expand: !this._isHorizontal, }); const sizerBox = new Clutter.Actor(); sizerBox.add_constraint(new Clutter.BindConstraint({ source: this._isHorizontal ? this._showAppsIcon.icon : this._dashContainer, coordinate: Clutter.BindCoordinate.HEIGHT, })); sizerBox.add_constraint(new Clutter.BindConstraint({ source: this._isHorizontal ? this._dashContainer : this._showAppsIcon.icon, coordinate: Clutter.BindCoordinate.WIDTH, })); this._background.add_child(sizerBox); this.add_child(this._background); this.add_child(this._dashContainer); this._workId = Main.initializeDeferredWork(this._box, this._redisplay.bind(this)); this._shellSettings = new Gio.Settings({ schema_id: 'org.gnome.shell', }); this._appSystem = Shell.AppSystem.get_default(); this.iconAnimator = new Docking.IconAnimator(this); this._signalsHandler.add([ this._appSystem, 'installed-changed', () => { AppFavorites.getAppFavorites().reload(); this._queueRedisplay(); }, ], [ AppFavorites.getAppFavorites(), 'changed', this._queueRedisplay.bind(this), ], [ this._appSystem, 'app-state-changed', this._queueRedisplay.bind(this), ], [ Main.overview, 'item-drag-begin', this._onItemDragBegin.bind(this), ], [ Main.overview, 'item-drag-end', this._onItemDragEnd.bind(this), ], [ Main.overview, 'item-drag-cancelled', this._onItemDragCancelled.bind(this), ], [ Main.overview, 'window-drag-begin', this._onWindowDragBegin.bind(this), ], [ Main.overview, 'window-drag-cancelled', this._onWindowDragEnd.bind(this), ], [ Main.overview, 'window-drag-end', this._onWindowDragEnd.bind(this), ]); this.connect('destroy', this._onDestroy.bind(this)); } vfunc_get_preferred_height(forWidth) { const [minHeight, natHeight] = super.vfunc_get_preferred_height.call(this, forWidth); if (!this._isHorizontal && this._maxHeight !== -1 && natHeight > this._maxHeight) return [minHeight, this._maxHeight]; else return [minHeight, natHeight]; } vfunc_get_preferred_width(forHeight) { const [minWidth, natWidth] = super.vfunc_get_preferred_width.call(this, forHeight); if (this._isHorizontal && this._maxWidth !== -1 && natWidth > this._maxWidth) return [minWidth, this._maxWidth]; else return [minWidth, natWidth]; } get _container() { return this._dashContainer; } _onDestroy() { this.iconAnimator.destroy(); if (this._requiresVisibilityTimeout) { GLib.source_remove(this._requiresVisibilityTimeout); delete this._requiresVisibilityTimeout; } if (this._ensureActorVisibilityTimeoutId) { GLib.source_remove(this._ensureActorVisibilityTimeoutId); delete this._ensureActorVisibilityTimeoutId; } } _onItemDragBegin(...args) { return Dash.Dash.prototype._onItemDragBegin.call(this, ...args); } _onItemDragCancelled(...args) { return Dash.Dash.prototype._onItemDragCancelled.call(this, ...args); } _onItemDragEnd(...args) { return Dash.Dash.prototype._onItemDragEnd.call(this, ...args); } _endItemDrag(...args) { return Dash.Dash.prototype._endItemDrag.call(this, ...args); } _onItemDragMotion(...args) { return Dash.Dash.prototype._onItemDragMotion.call(this, ...args); } _appIdListToHash(...args) { return Dash.Dash.prototype._appIdListToHash.call(this, ...args); } _queueRedisplay(...args) { return Dash.Dash.prototype._queueRedisplay.call(this, ...args); } _hookUpLabel(...args) { return Dash.Dash.prototype._hookUpLabel.call(this, ...args); } _syncLabel(...args) { return Dash.Dash.prototype._syncLabel.call(this, ...args); } _clearDragPlaceholder(...args) { return Dash.Dash.prototype._clearDragPlaceholder.call(this, ...args); } _clearEmptyDropTarget(...args) { return Dash.Dash.prototype._clearEmptyDropTarget.call(this, ...args); } handleDragOver(source, actor, x, y, time) { let ret; if (this._isHorizontal) { ret = Dash.Dash.prototype.handleDragOver.call(this, source, actor, x, y, time); if (ret === DND.DragMotionResult.CONTINUE) return ret; } else { const propertyInjections = new Utils.PropertyInjectionsHandler(); propertyInjections.add(this._box, 'width', { get: () => this._box.get_children().reduce((a, c) => a + c.height, 0), }); if (this._dragPlaceholder) { propertyInjections.add(this._dragPlaceholder, 'width', { get: () => this._dragPlaceholder.height, }); } ret = Dash.Dash.prototype.handleDragOver.call(this, source, actor, y, x, time); propertyInjections.destroy(); if (ret === DND.DragMotionResult.CONTINUE) return ret; if (this._dragPlaceholder) { this._dragPlaceholder.child.set_width(this.iconSize / 2); this._dragPlaceholder.child.set_height(this.iconSize); let pos = this._dragPlaceholderPos; if (this._isHorizontal && Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) pos = this._box.get_children() - 1 - pos; if (pos !== this._dragPlaceholderPos) { this._dragPlaceholderPos = pos; this._box.set_child_at_index(this._dragPlaceholder, this._dragPlaceholderPos); } } } if (this._dragPlaceholder) { // Ensure the next and previous icon are visible when moving the // placeholder (we're assuming there's room for both of them) const children = this._box.get_children(); if (this._dragPlaceholderPos > 0) { ensureActorVisibleInScrollView(this._scrollView, children[this._dragPlaceholderPos - 1]); } if (this._dragPlaceholderPos >= -1 && this._dragPlaceholderPos < children.length - 1) { ensureActorVisibleInScrollView(this._scrollView, children[this._dragPlaceholderPos + 1]); } } return ret; } acceptDrop(...args) { return Dash.Dash.prototype.acceptDrop.call(this, ...args); } _onWindowDragBegin(...args) { return Dash.Dash.prototype._onWindowDragBegin.call(this, ...args); } _onWindowDragEnd(...args) { return Dash.Dash.prototype._onWindowDragEnd.call(this, ...args); } _onScrollEvent(actor, event) { // If scroll is not used because the icon is resized, let the scroll event propagate. if (!Docking.DockManager.settings.iconSizeFixed) return Clutter.EVENT_PROPAGATE; // reset timeout to avid conflicts with the mousehover event this._ensureItemVisibility(null); // Skip to avoid double events mouse // TODO: Horizontal events are emulated, potentially due to a conflict // with the workspace switching gesture. if (!this._isHorizontal && event.is_pointer_emulated()) return Clutter.EVENT_STOP; let adjustment, delta = 0; if (this._isHorizontal) adjustment = this._scrollView.get_hscroll_bar().get_adjustment(); else adjustment = this._scrollView.get_vscroll_bar().get_adjustment(); const increment = adjustment.step_increment; if (this._isHorizontal) { switch (event.get_scroll_direction()) { case Clutter.ScrollDirection.LEFT: delta = -increment; break; case Clutter.ScrollDirection.RIGHT: delta = Number(increment); break; case Clutter.ScrollDirection.SMOOTH: { const [dx] = event.get_scroll_delta(); delta = dx * increment; break; } } } else { switch (event.get_scroll_direction()) { case Clutter.ScrollDirection.UP: delta = -increment; break; case Clutter.ScrollDirection.DOWN: delta = Number(increment); break; case Clutter.ScrollDirection.SMOOTH: { const [, dy] = event.get_scroll_delta(); delta = dy * increment; break; } } } const value = adjustment.get_value(); // TODO: Remove this if possible. if (Number.isNaN(value)) adjustment.set_value(delta); else adjustment.set_value(value + delta); return Clutter.EVENT_STOP; } _ensureItemVisibility(actor) { if (actor?.hover) { const destroyId = actor.connect('destroy', () => this._ensureItemVisibility(null)); this._ensureActorVisibilityTimeoutId = GLib.timeout_add( GLib.PRIORITY_DEFAULT, 100, () => { actor.disconnect(destroyId); ensureActorVisibleInScrollView(this._scrollView, actor); this._ensureActorVisibilityTimeoutId = 0; return GLib.SOURCE_REMOVE; }); } else if (this._ensureActorVisibilityTimeoutId) { GLib.source_remove(this._ensureActorVisibilityTimeoutId); this._ensureActorVisibilityTimeoutId = 0; } } _createAppItem(app) { const appIcon = new AppIcons.makeAppIcon(app, this._monitorIndex, this.iconAnimator); if (appIcon._draggable) { appIcon._draggable.connect('drag-begin', () => { appIcon.opacity = 50; }); appIcon._draggable.connect('drag-end', () => { appIcon.opacity = 255; }); } appIcon.connect('menu-state-changed', (_, opened) => { this._itemMenuStateChanged(item, opened); }); const item = new DockDashItemContainer(this._position); item.setChild(appIcon); appIcon.connect('notify::hover', a => this._ensureItemVisibility(a)); appIcon.connect('clicked', actor => { ensureActorVisibleInScrollView(this._scrollView, actor); }); appIcon.connect('key-focus-in', actor => { const [xShift, yShift] = ensureActorVisibleInScrollView(this._scrollView, actor); // This signal is triggered also by mouse click. The popup menu is opened at the original // coordinates. Thus correct for the shift which is going to be applied to the scrollview. if (appIcon._menu) { appIcon._menu._boxPointer.xOffset = -xShift; appIcon._menu._boxPointer.yOffset = -yShift; } }); appIcon.connect('notify::focused', () => { const {settings} = Docking.DockManager; if (appIcon.focused && settings.scrollToFocusedApplication) ensureActorVisibleInScrollView(this._scrollView, item); }); appIcon.connect('notify::urgent', () => { if (appIcon.urgent) { ensureActorVisibleInScrollView(this._scrollView, item); if (Docking.DockManager.settings.showDockUrgentNotify) this._requireVisibility(); } }); // Override default AppIcon label_actor, now the // accessible_name is set at DashItemContainer.setLabelText appIcon.label_actor = null; item.setLabelText(app.get_name()); appIcon.icon.setIconSize(this.iconSize); this._hookUpLabel(item, appIcon); item.connect('notify::position', () => appIcon.updateIconGeometry()); item.connect('notify::size', () => appIcon.updateIconGeometry()); return item; } _requireVisibility() { this.requiresVisibility = true; if (this._requiresVisibilityTimeout) GLib.source_remove(this._requiresVisibilityTimeout); this._requiresVisibilityTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, DASH_VISIBILITY_TIMEOUT, () => { this._requiresVisibilityTimeout = 0; this.requiresVisibility = false; }); } /** * Return an array with the "proper" appIcons currently in the dash */ getAppIcons() { // Only consider children which are "proper" // icons (i.e. ignoring drag placeholders) and which are not // animating out (which means they will be destroyed at the end of // the animation) const iconChildren = this._box.get_children().filter(actor => { return actor.child && !!actor.child.icon && !actor.animatingOut; }); const appIcons = iconChildren.map(actor => { return actor.child; }); return appIcons; } _itemMenuStateChanged(item, opened) { Dash.Dash.prototype._itemMenuStateChanged.call(this, item, opened); if (opened) { this.emit('menu-opened'); } else { // I want to listen from outside when a menu is closed. I used to // add a custom signal to the appIcon, since gnome 3.8 the signal // calling this callback was added upstream. this.emit('menu-closed'); } } _adjustIconSize() { // For the icon size, we only consider children which are "proper" // icons (i.e. ignoring drag placeholders) and which are not // animating out (which means they will be destroyed at the end of // the animation) const iconChildren = this._box.get_children().filter(actor => { return actor.child && actor.child._delegate && actor.child._delegate.icon && !actor.animatingOut; }); iconChildren.push(this._showAppsIcon); if (this._maxWidth === -1 && this._maxHeight === -1) return; // Check if the container is present in the stage. This avoids critical // errors when unlocking the screen if (!this._container.get_stage()) return; const themeNode = this._dashContainer.get_theme_node(); const maxAllocation = new Clutter.ActorBox({ x1: 0, y1: 0, x2: this._isHorizontal ? this._maxWidth : 42 /* whatever */, y2: this._isHorizontal ? 42 : this._maxHeight, }); const maxContent = themeNode.get_content_box(maxAllocation); let availSpace; if (this._isHorizontal) availSpace = maxContent.get_width(); else availSpace = maxContent.get_height(); const spacing = themeNode.get_length('spacing'); const [{child: firstButton}] = iconChildren; const {child: firstIcon} = firstButton?.icon ?? {child: null}; // if no icons there's nothing to adjust if (!firstIcon) return; // Enforce valid spacings during the size request firstIcon.ensure_style(); const [, , iconWidth, iconHeight] = firstIcon.get_preferred_size(); const [, , buttonWidth, buttonHeight] = firstButton.get_preferred_size(); if (this._isHorizontal) { // Subtract icon padding and box spacing from the available width availSpace -= iconChildren.length * (buttonWidth - iconWidth) + (iconChildren.length - 1) * spacing; if (this._separator) { const [, , separatorWidth] = this._separator.get_preferred_size(); availSpace -= separatorWidth + spacing; } } else { // Subtract icon padding and box spacing from the available height availSpace -= iconChildren.length * (buttonHeight - iconHeight) + (iconChildren.length - 1) * spacing; if (this._separator) { const [, , , separatorHeight] = this._separator.get_preferred_size(); availSpace -= separatorHeight + spacing; } } const maxIconSize = availSpace / iconChildren.length; const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); const iconSizes = this._availableIconSizes.map(s => s * scaleFactor); let [newIconSize] = this._availableIconSizes; for (let i = 0; i < iconSizes.length; i++) { if (iconSizes[i] <= maxIconSize) newIconSize = this._availableIconSizes[i]; } if (newIconSize === this.iconSize) return; const oldIconSize = this.iconSize; this.iconSize = newIconSize; this.emit('icon-size-changed'); const scale = oldIconSize / newIconSize; for (let i = 0; i < iconChildren.length; i++) { const {icon} = iconChildren[i].child._delegate; // Set the new size immediately, to keep the icons' sizes // in sync with this.iconSize icon.setIconSize(this.iconSize); // Don't animate the icon size change when the overview // is transitioning, not visible or when initially filling // the dash if (!Main.overview.visible || Main.overview.animationInProgress || !this._shownInitially) continue; const [targetWidth, targetHeight] = icon.icon.get_size(); // Scale the icon's texture to the previous size and // tween to the new size icon.icon.set_size(icon.icon.width * scale, icon.icon.height * scale); icon.icon.ease({ width: targetWidth, height: targetHeight, duration: DASH_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } if (this._separator) { const animateProperties = this._isHorizontal ? {height: this.iconSize} : {width: this.iconSize}; this._separator.ease({ ...animateProperties, duration: DASH_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } } _redisplay() { const favorites = AppFavorites.getAppFavorites().getFavoriteMap(); let running = this._appSystem.get_running(); const dockManager = Docking.DockManager.getDefault(); const {settings} = dockManager; this._scrollView.set({ xAlign: Clutter.ActorAlign.FILL, yAlign: Clutter.ActorAlign.FILL, }); if (dockManager.settings.dockExtended) { if (!this._isHorizontal) { this._scrollView.yAlign = dockManager.settings.alwaysCenterIcons ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START; } else { this._scrollView.xAlign = dockManager.settings.alwaysCenterIcons ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START; } } if (settings.isolateWorkspaces || settings.isolateMonitors) { // When using isolation, we filter out apps that have no windows in // the current workspace const monitorIndex = this._monitorIndex; running = running.filter(app => AppIcons.getInterestingWindows(app.get_windows(), monitorIndex).length); } const children = this._box.get_children().filter(actor => { return actor.child && actor.child._delegate && actor.child._delegate.app; }); // Apps currently in the dash let oldApps = children.map(actor => actor.child._delegate.app); // Apps supposed to be in the dash const newApps = []; const {showFavorites} = settings; if (showFavorites) newApps.push(...Object.values(favorites)); if (settings.showRunning) { // We reorder the running apps so that they don't change position on the // dash with every redisplay() call // First: add the apps from the oldApps list that are still running oldApps.forEach(oldApp => { const index = running.indexOf(oldApp); if (index > -1) { const [app] = running.splice(index, 1); if (!showFavorites || !(app.get_id() in favorites)) newApps.push(app); } }); // Second: add the new apps running.forEach(app => { if (!showFavorites || !(app.get_id() in favorites)) newApps.push(app); }); } this._signalsHandler.removeWithLabel(Labels.SHOW_MOUNTS); if (dockManager.removables) { this._signalsHandler.addWithLabel(Labels.SHOW_MOUNTS, dockManager.removables, 'changed', this._queueRedisplay.bind(this)); dockManager.removables.getApps().forEach(removable => { if (!newApps.includes(removable)) newApps.push(removable); }); } else { oldApps = oldApps.filter(app => !app.location || app.isTrash); } if (dockManager.trash) { const trashApp = dockManager.trash.getApp(); if (!newApps.includes(trashApp)) newApps.push(trashApp); } else { oldApps = oldApps.filter(app => !app.isTrash); } // Temporary remove the separator so that we don't compute to position icons const oldSeparatorPos = this._box.get_children().indexOf(this._separator); if (this._separator) this._box.remove_child(this._separator); // Figure out the actual changes to the list of items; we iterate // over both the list of items currently in the dash and the list // of items expected there, and collect additions and removals. // Moves are both an addition and a removal, where the order of // the operations depends on whether we encounter the position // where the item has been added first or the one from where it // was removed. // There is an assumption that only one item is moved at a given // time; when moving several items at once, everything will still // end up at the right position, but there might be additional // additions/removals (e.g. it might remove all the launchers // and add them back in the new order even if a smaller set of // additions and removals is possible). // If above assumptions turns out to be a problem, we might need // to use a more sophisticated algorithm, e.g. Longest Common // Subsequence as used by diff. const addedItems = []; const removedActors = []; let newIndex = 0; let oldIndex = 0; while (newIndex < newApps.length || oldIndex < oldApps.length) { const oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null; const newApp = newApps.length > newIndex ? newApps[newIndex] : null; // No change at oldIndex/newIndex if (oldApp === newApp) { oldIndex++; newIndex++; continue; } // App removed at oldIndex if (oldApp && !newApps.includes(oldApp)) { removedActors.push(children[oldIndex]); oldIndex++; continue; } // App added at newIndex if (newApp && !oldApps.includes(newApp)) { addedItems.push({ app: newApp, item: this._createAppItem(newApp), pos: newIndex, }); newIndex++; continue; } // App moved const nextApp = newApps.length > newIndex + 1 ? newApps[newIndex + 1] : null; const insertHere = nextApp && nextApp === oldApp; const alreadyRemoved = removedActors.reduce((result, actor) => { const removedApp = actor.child._delegate.app; return result || removedApp === newApp; }, false); if (insertHere || alreadyRemoved) { const newItem = this._createAppItem(newApp); addedItems.push({ app: newApp, item: newItem, pos: newIndex + removedActors.length, }); newIndex++; } else { removedActors.push(children[oldIndex]); oldIndex++; } } for (let i = 0; i < addedItems.length; i++) { this._box.insert_child_at_index(addedItems[i].item, addedItems[i].pos); } for (let i = 0; i < removedActors.length; i++) { const item = removedActors[i]; // Don't animate item removal when the overview is transitioning // or hidden if (!Main.overview.animationInProgress) item.animateOutAndDestroy(); else item.destroy(); } // Update separator const nFavorites = Object.keys(favorites).length; const nIcons = children.length + addedItems.length - removedActors.length; if (nFavorites > 0 && nFavorites < nIcons) { if (!this._separator) { this._separator = new St.Widget({ style_class: 'dash-separator', x_align: this._isHorizontal ? Clutter.ActorAlign.FILL : Clutter.ActorAlign.CENTER, y_align: this._isHorizontal ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.FILL, width: this._isHorizontal ? -1 : this.iconSize, height: this._isHorizontal ? this.iconSize : -1, reactive: true, track_hover: true, }); this._separator.connect('notify::hover', a => this._ensureItemVisibility(a)); } let pos = nFavorites + this._animatingPlaceholdersCount; if (this._dragPlaceholder) pos++; const removedFavorites = removedActors.filter(a => children.indexOf(a) < oldSeparatorPos); pos += removedFavorites.length; this._box.insert_child_at_index(this._separator, pos); } else if (this._separator) { this._separator.destroy(); this._separator = null; } this._adjustIconSize(); // Skip animations on first run when adding the initial set // of items, to avoid all items zooming in at once const animate = this._shownInitially && !Main.layoutManager._startingUp; if (!this._shownInitially) this._shownInitially = true; addedItems.forEach(({item}) => item.show(animate)); // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 // Without it, StBoxLayout may use a stale size cache this._box.queue_relayout(); // This will update the size, and the corresponding number for each icon this._updateNumberOverlay(); this.updateShowAppsButton(); } _updateNumberOverlay() { const appIcons = this.getAppIcons(); let counter = 1; appIcons.forEach(icon => { if (counter < 10) { icon.setNumberOverlay(counter); counter++; } else if (counter === 10) { icon.setNumberOverlay(0); counter++; } else { // No overlay after 10 icon.setNumberOverlay(-1); } icon.updateNumberOverlay(); }); } toggleNumberOverlay(activate) { const appIcons = this.getAppIcons(); appIcons.forEach(icon => { icon.toggleNumberOverlay(activate); }); } _initializeIconSize(maxSize) { const maxAllowed = baseIconSizes[baseIconSizes.length - 1]; maxSize = Math.min(maxSize, maxAllowed); if (Docking.DockManager.settings.iconSizeFixed) { this._availableIconSizes = [maxSize]; } else { this._availableIconSizes = baseIconSizes.filter(val => { return val < maxSize; }); this._availableIconSizes.push(maxSize); } } setIconSize(maxSize, doNotAnimate) { this._initializeIconSize(maxSize); if (doNotAnimate) this._shownInitially = false; this._queueRedisplay(); } /** * Reset the displayed apps icon to maintain the correct order when changing * show favorites/show running settings */ resetAppIcons() { const children = this._box.get_children().filter(actor => { return actor.child && !!actor.child.icon; }); for (let i = 0; i < children.length; i++) { const item = children[i]; item.destroy(); } // to avoid ugly animations, just suppress them like when dash is first loaded. this._shownInitially = false; this._redisplay(); } get showAppsButton() { return this._showAppsIcon.toggleButton; } showShowAppsButton() { this._showAppsIcon.visible = true; this._showAppsIcon.show(true); this.updateShowAppsButton(); } hideShowAppsButton() { this._showAppsIcon.visible = false; } setMaxSize(maxWidth, maxHeight) { if (this._maxWidth === maxWidth && this._maxHeight === maxHeight) return; this._maxWidth = maxWidth; this._maxHeight = maxHeight; this._queueRedisplay(); } updateShowAppsButton() { if (this._showAppsIcon.get_parent() && !this._showAppsIcon.visible) return; const {settings} = Docking.DockManager; const notifiedProperties = []; const showAppsContainer = settings.showAppsAlwaysInTheEdge || !settings.dockExtended ? this._dashContainer : this._boxContainer; this._signalsHandler.addWithLabel(Labels.FIRST_LAST_CHILD_WORKAROUND, showAppsContainer, 'notify', (_obj, pspec) => notifiedProperties.push(pspec.name)); if (this._showAppsIcon.get_parent() !== showAppsContainer) { this._showAppsIcon.get_parent()?.remove_child(this._showAppsIcon); if (Docking.DockManager.settings.showAppsAtTop) showAppsContainer.insert_child_below(this._showAppsIcon, null); else showAppsContainer.insert_child_above(this._showAppsIcon, null); } else if (settings.showAppsAtTop) { showAppsContainer.set_child_below_sibling(this._showAppsIcon, null); } else { showAppsContainer.set_child_above_sibling(this._showAppsIcon, null); } this._signalsHandler.removeWithLabel(Labels.FIRST_LAST_CHILD_WORKAROUND); // This is indeed ugly, but we need to ensure that the last and first // visible widgets are re-computed by St, that is buggy because of a // mutter issue that is being fixed: // https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2047 if (!notifiedProperties.includes('first-child')) showAppsContainer.notify('first-child'); if (!notifiedProperties.includes('last-child')) showAppsContainer.notify('last-child'); } }); /** * This is a copy of the same function in utils.js, but also adjust horizontal scrolling * and perform few further checks on the current value to avoid changing the values when * it would be clamp to the current one in any case. * Return the amount of shift applied * * @param scrollView * @param actor */ function ensureActorVisibleInScrollView(scrollView, actor) { // access to scrollView.[hv]scroll was deprecated in gnome 46 // instead, adjustment can be accessed directly // keep old way for backwards compatibility (gnome <= 45) const vAdjustment = scrollView.vadjustment ?? scrollView.vscroll.adjustment; const hAdjustment = scrollView.hadjustment ?? scrollView.hscroll.adjustment; const {value: vValue0, pageSize: vPageSize, upper: vUpper} = vAdjustment; const {value: hValue0, pageSize: hPageSize, upper: hUpper} = hAdjustment; let [hValue, vValue] = [hValue0, vValue0]; let vOffset = 0; let hOffset = 0; const fade = scrollView.get_effect('fade'); if (fade) { vOffset = fade.fade_margins.top; hOffset = fade.fade_margins.left; } const box = actor.get_allocation_box(); let {y1} = box, {y2} = box, {x1} = box, {x2} = box; let parent = actor.get_parent(); while (parent !== scrollView) { if (!parent) throw new Error('Actor not in scroll view'); const parentBox = parent.get_allocation_box(); y1 += parentBox.y1; y2 += parentBox.y1; x1 += parentBox.x1; x2 += parentBox.x1; parent = parent.get_parent(); } if (y1 < vValue + vOffset) vValue = Math.max(0, y1 - vOffset); else if (vValue < vUpper - vPageSize && y2 > vValue + vPageSize - vOffset) vValue = Math.min(vUpper - vPageSize, y2 + vOffset - vPageSize); if (x1 < hValue + hOffset) hValue = Math.max(0, x1 - hOffset); else if (hValue < hUpper - hPageSize && x2 > hValue + hPageSize - hOffset) hValue = Math.min(hUpper - hPageSize, x2 + hOffset - hPageSize); if (vValue !== vValue0) { vAdjustment.ease(vValue, { mode: Clutter.AnimationMode.EASE_OUT_QUAD, duration: Util.SCROLL_TIME, }); } if (hValue !== hValue0) { hAdjustment.ease(hValue, { mode: Clutter.AnimationMode.EASE_OUT_QUAD, duration: Util.SCROLL_TIME, }); } return [hValue - hValue0, vValue - vValue0]; }