diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-09-16 17:09:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-09-16 17:09:30 +0000 |
commit | ecc508e4d31d0a7eff3e312f8bfa0b5354bf67c5 (patch) | |
tree | 4ab2de3e20af5191ab1d174848bde3e56b68200e /extensions/47/vertical-workspaces/lib/dash.js | |
parent | Updating 46/vertical-workspaces to version 46.2+20240828 [5b87af5]. (diff) | |
download | gnome-shell-extensions-extra-ecc508e4d31d0a7eff3e312f8bfa0b5354bf67c5.tar.xz gnome-shell-extensions-extra-ecc508e4d31d0a7eff3e312f8bfa0b5354bf67c5.zip |
Renaming extensions subdirectory for GNOME 47.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'extensions/47/vertical-workspaces/lib/dash.js')
-rw-r--r-- | extensions/47/vertical-workspaces/lib/dash.js | 1307 |
1 files changed, 1307 insertions, 0 deletions
diff --git a/extensions/47/vertical-workspaces/lib/dash.js b/extensions/47/vertical-workspaces/lib/dash.js new file mode 100644 index 0000000..c7ebbff --- /dev/null +++ b/extensions/47/vertical-workspaces/lib/dash.js @@ -0,0 +1,1307 @@ +/** + * V-Shell (Vertical Workspaces) + * dash.js + * + * @author GdH <G-dH@github.com> + * @copyright 2022-2024 + * @license GPL-3.0 + */ + +'use strict'; + +import Clutter from 'gi://Clutter'; +import GLib from 'gi://GLib'; +import Meta from 'gi://Meta'; +import Shell from 'gi://Shell'; +import St from 'gi://St'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as Dash from 'resource:///org/gnome/shell/ui/dash.js'; +import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js'; +import * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js'; +import * as AppMenu from 'resource:///org/gnome/shell/ui/appMenu.js'; +import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js'; +import * as DND from 'resource:///org/gnome/shell/ui/dnd.js'; +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; + +let Me; +let opt; +// gettext +let _; + +let _moduleEnabled; +let _timeouts; + +// added values to achieve a better ability to scale down according to available space +export const BaseIconSizes = [16, 24, 32, 40, 44, 48, 56, 64, 72, 80, 96, 112, 128]; + +const DASH_ITEM_LABEL_SHOW_TIME = 150; + +const shellVersion46 = !Clutter.Container; // Container has been removed in 46 + +export const DashModule = class { + constructor(me) { + Me = me; + opt = Me.opt; + _ = Me.gettext; + + this._firstActivation = true; + this.moduleEnabled = false; + this._overrides = null; + this._horizontalWorkId = null; + this._verticalWorkId = null; + this._showAppsIconBtnPressId = 0; + } + + cleanGlobals() { + Me = null; + opt = null; + _ = null; + } + + update(reset) { + this._removeTimeouts(); + + this.moduleEnabled = opt.get('dashModule'); + const conflict = !!(Me.Util.getEnabledExtensions('dash-to-dock').length || + Me.Util.getEnabledExtensions('dash2dock').length || + Me.Util.getEnabledExtensions('ubuntu-dock').length || + Me.Util.getEnabledExtensions('dash-to-panel').length); + + if (conflict && !reset) + console.warn(`[${Me.metadata.name}] Warning: "Dash" module disabled due to potential conflict with another extension`); + + reset = reset || !this.moduleEnabled || conflict; + this._conflict = conflict; + + // don't touch the original code if module disabled + if (reset && !this._firstActivation) { + this._disableModule(); + } else if (!reset) { + this._firstActivation = false; + this._activateModule(); + } + if (reset && this._firstActivation) + console.debug(' DashModule - Keeping untouched'); + } + + updateStyle(dash) { + if (opt.DASH_BG_LIGHT) + dash._background.add_style_class_name('dash-background-light'); + else + dash._background.remove_style_class_name('dash-background-light'); + + dash._background.opacity = opt.DASH_BG_OPACITY; + let radius = opt.DASH_BG_RADIUS; + if (radius) { + let style; + switch (opt.DASH_POSITION) { + case 1: + style = opt.DASH_BG_GS3_STYLE ? `border-radius: ${radius}px 0 0 ${radius}px;` : `border-radius: ${radius}px;`; + break; + case 3: + style = opt.DASH_BG_GS3_STYLE ? `border-radius: 0 ${radius}px ${radius}px 0;` : `border-radius: ${radius}px;`; + break; + default: + style = `border-radius: ${radius}px;`; + } + dash._background.set_style(style); + } else { + dash._background.set_style(''); + } + } + + _activateModule() { + _moduleEnabled = true; + _timeouts = {}; + const dash = Main.overview._overview._controls.layoutManager._dash; + + if (!this._originalWorkId) + this._originalWorkId = dash._workId; + + if (!this._overrides) + this._overrides = new Me.Util.Overrides(); + + this._resetStyle(dash); + this.updateStyle(dash); + + this._overrides.addOverride('DashItemContainer', Dash.DashItemContainer.prototype, DashItemContainerCommon); + this._overrides.addOverride('DashCommon', Dash.Dash.prototype, DashCommon); + this._overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIconCommon); + this._overrides.addOverride('DashIcon', Dash.DashIcon.prototype, DashIconCommon); + this._overrides.addOverride('AppMenu', AppMenu.AppMenu.prototype, AppMenuCommon); + + if (shellVersion46) + dash.add_style_class_name('dash-46'); + + if (opt.DASH_VERTICAL) { + // this._overrides.addOverride('Dash', Dash.Dash.prototype, DashVerticalOverride); + dash.add_style_class_name(shellVersion46 + ? 'vertical-46' + : 'vertical' + ); + + this._setOrientation(Clutter.Orientation.VERTICAL); + } else { + this._setOrientation(Clutter.Orientation.HORIZONTAL); + } + + if (opt.DASH_VERTICAL && opt.DASH_BG_GS3_STYLE) { + if (opt.DASH_LEFT) { + dash.add_style_class_name(shellVersion46 + ? 'vertical-46-gs3-left' + : 'vertical-gs3-left'); + } else if (opt.DASH_RIGHT) { + dash.add_style_class_name(shellVersion46 + ? 'vertical-46-gs3-right' + : 'vertical-gs3-right'); + } + } else { + dash.remove_style_class_name('vertical-gs3-left'); + dash.remove_style_class_name('vertical-gs3-right'); + dash.remove_style_class_name('vertical-46-gs3-left'); + dash.remove_style_class_name('vertical-46-gs3-right'); + } + + if (!this._customWorkId) + this._customWorkId = Main.initializeDeferredWork(dash._box, dash._redisplay.bind(dash)); + dash._workId = this._customWorkId; + + this._moveDashAppGridIcon(); + this._connectShowAppsIcon(); + + dash.visible = opt.DASH_VISIBLE; + // dash._background.add_style_class_name('dash-background-reduced'); + dash._queueRedisplay(); + + if (opt.DASH_ISOLATE_WS && !this._wmSwitchWsConId) { + this._wmSwitchWsConId = global.windowManager.connect('switch-workspace', () => dash._queueRedisplay()); + this._newWindowConId = global.display.connect_after('window-created', () => dash._queueRedisplay()); + } + + console.debug(' DashModule - Activated'); + } + + _disableModule() { + const dash = Main.overview._overview._controls.layoutManager._dash; + + if (this._overrides) + this._overrides.removeAll(); + this._overrides = null; + + dash._workId = this._originalWorkId; + + if (this._wmSwitchWsConId) { + global.windowManager.disconnect(this._wmSwitchWsConId); + this._wmSwitchWsConId = 0; + } + if (this._newWindowConId) { + global.windowManager.disconnect(this._newWindowConId); + this._newWindowConId = 0; + } + + const reset = true; + this._setOrientation(Clutter.Orientation.HORIZONTAL); + this._moveDashAppGridIcon(reset); + this._connectShowAppsIcon(reset); + + this._resetStyle(dash); + dash.visible = !this._conflict; + dash._background.opacity = 255; + + _moduleEnabled = false; + console.debug(' DashModule - Disabled'); + } + + _resetStyle(dash) { + dash.remove_style_class_name('dash-46'); + dash.remove_style_class_name('vertical'); + dash.remove_style_class_name('vertical-46'); + dash.remove_style_class_name('vertical-gs3-left'); + dash.remove_style_class_name('vertical-gs3-right'); + dash.remove_style_class_name('vertical-46-gs3-left'); + dash.remove_style_class_name('vertical-46-gs3-right'); + dash.remove_style_class_name('vertical-left'); + dash.remove_style_class_name('vertical-right'); + dash._background.remove_style_class_name('dash-background-light'); + dash._background.remove_style_class_name('dash-background-reduced'); + dash._background.set_style(''); + } + + _removeTimeouts() { + if (_timeouts) { + Object.values(_timeouts).forEach(t => { + if (t) + GLib.source_remove(t); + }); + _timeouts = null; + } + } + + _setOrientation(orientation, dash) { + dash = dash ?? Main.overview._overview._controls.layoutManager._dash; + + dash._box.layout_manager.orientation = orientation; + dash._dashContainer.layout_manager.orientation = orientation; + dash._dashContainer.y_expand = !orientation; + dash._dashContainer.x_expand = !!orientation; + dash.x_align = orientation ? Clutter.ActorAlign.START : Clutter.ActorAlign.CENTER; + dash.y_align = orientation ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.FILL; + + let sizerBox = dash._background.get_children()[0]; + sizerBox.clear_constraints(); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: dash._showAppsIcon.icon, + coordinate: orientation ? Clutter.BindCoordinate.WIDTH : Clutter.BindCoordinate.HEIGHT, + })); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: dash._dashContainer, + coordinate: orientation ? Clutter.BindCoordinate.HEIGHT : Clutter.BindCoordinate.WIDTH, + })); + dash._box.remove_all_children(); + dash._separator = null; + dash._queueRedisplay(); + dash._adjustIconSize(); + } + + _moveDashAppGridIcon(reset = false) { + // move dash app grid icon to the front + const dash = Main.overview._overview._controls.layoutManager._dash; + + const appIconPosition = opt.get('showAppsIconPosition'); + dash._showAppsIcon.remove_style_class_name('show-apps-icon-vertical-hide'); + dash._showAppsIcon.remove_style_class_name('show-apps-icon-horizontal-hide'); + dash._showAppsIcon.opacity = 255; + if (!reset && appIconPosition === 0) // 0 - start + dash._dashContainer.set_child_at_index(dash._showAppsIcon, 0); + if (reset || appIconPosition === 1) { // 1 - end + const index = dash._dashContainer.get_children().length - 1; + dash._dashContainer.set_child_at_index(dash._showAppsIcon, index); + } + if (!reset && appIconPosition === 2) { // 2 - hide + const style = opt.DASH_VERTICAL ? 'show-apps-icon-vertical-hide' : 'show-apps-icon-horizontal-hide'; + dash._showAppsIcon.add_style_class_name(style); + // for some reason even if the icon height in vertical mode should be set to 0 by the style, it stays visible in full size returning height 1px + dash._showAppsIcon.opacity = 0; + } + } + + _connectShowAppsIcon(reset = false, dash) { + dash = dash ?? Main.overview._overview._controls.layoutManager._dash; + if (!reset) { + if (this._showAppsIconBtnPressId || Me.Util.dashIsDashToDock()) { + // button is already connected || dash is Dash to Dock + return; + } + dash._showAppsIcon.reactive = true; + this._showAppsIconBtnPressId = dash._showAppsIcon.connect('button-press-event', (actor, event) => { + const button = event.get_button(); + if (button === Clutter.BUTTON_MIDDLE) + Me.Util.openPreferences(); + else if (button === Clutter.BUTTON_SECONDARY) + Me.Util.activateSearchProvider(Me.WSP_PREFIX); + else + return Clutter.EVENT_PROPAGATE; + return Clutter.EVENT_STOP; + }); + } else if (this._showAppsIconBtnPressId) { + dash._showAppsIcon.disconnect(this._showAppsIconBtnPressId); + this._showAppsIconBtnPressId = 0; + dash._showAppsIcon.reactive = false; + } + } +}; + +function getAppFromSource(source) { + if (source instanceof AppDisplay.AppIcon) + return source.app; + else + return null; +} + +const DashItemContainerCommon = { + // move labels according dash position + showLabel() { + if (!this._labelText) + return; + + const windows = this.child.app?.get_windows(); + const recentWindowTitle = windows && windows.length ? windows[0].get_title() : ''; + const windowCount = this.child.app?.get_windows().length; + let labelSuffix = ''; + if (windowCount > 1) + labelSuffix = ` (${windowCount})`; + if (recentWindowTitle && recentWindowTitle !== this._labelText) + labelSuffix += `\n ${recentWindowTitle}`; + + this.label.set_text(this._labelText + labelSuffix); + + this.label.opacity = 0; + this.label.show(); + + let [stageX, stageY] = this.get_transformed_position(); + + const itemWidth = this.allocation.get_width(); + const itemHeight = this.allocation.get_height(); + + const labelWidth = this.label.get_width(); + const labelHeight = this.label.get_height(); + let xOffset = Math.floor((itemWidth - labelWidth) / 2); + let x = Math.clamp(stageX + xOffset, 0, global.stage.width - labelWidth); + const primaryMonitor = global.display.get_monitor_geometry(global.display.get_primary_monitor()); + x = Math.clamp(x, primaryMonitor.x, primaryMonitor.x + primaryMonitor.width - labelWidth); + + let node = this.label.get_theme_node(); + let y; + + if (opt.DASH_TOP) { + const yOffset = itemHeight + (shellVersion46 ? 0 : -3); + y = stageY + yOffset; + } else if (opt.DASH_BOTTOM) { + const yOffset = node.get_length('-y-offset'); + y = stageY - this.label.height - yOffset; + } else if (opt.DASH_RIGHT) { + const yOffset = Math.floor((itemHeight - labelHeight) / 2); + xOffset = shellVersion46 ? 8 : 4; + + x = stageX - xOffset - this.label.width; + y = Math.clamp(stageY + yOffset, 0, global.stage.height - labelHeight); + } else if (opt.DASH_LEFT) { + const yOffset = Math.floor((itemHeight - labelHeight) / 2); + xOffset = shellVersion46 ? 8 : 4; + + x = stageX + this.width + xOffset; + y = Math.clamp(stageY + yOffset, 0, global.stage.height - labelHeight); + } + + this.label.set_position(x, y); + this.label.ease({ + opacity: 255, + duration: DASH_ITEM_LABEL_SHOW_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + this.label.set_position(x, y); + this.label.ease({ + opacity: 255, + duration: DASH_ITEM_LABEL_SHOW_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + }, +}; + +const DashCommon = { + _redisplay() { + // After disabling V-Shell queueRedisplay() may call this function + // In that case redirect the call to the current _redisplay() + if (!_moduleEnabled) { + this._redisplay(); + return; + } + + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + let running = this._appSystem.get_running(); + + if (opt.DASH_ISOLATE_WS) { + const currentWs = global.workspace_manager.get_active_workspace(); + running = running.filter(app => { + return app.get_windows().filter(w => w.get_workspace() === currentWs).length; + }); + this._box.get_children().forEach(a => a.child?._updateRunningStyle()); + } + + let 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 + let newApps = []; + + for (let id in favorites) + newApps.push(favorites[id]); + + for (let i = 0; i < running.length; i++) { + let app = running[i]; + if (app.get_id() in favorites) + continue; + newApps.push(app); + } + + // 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. + let addedItems = []; + let removedActors = []; + + let newIndex = 0; + let oldIndex = 0; + while (newIndex < newApps.length || oldIndex < oldApps.length) { + let oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null; + let 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 + let nextApp = newApps.length > newIndex + 1 + ? newApps[newIndex + 1] : null; + let insertHere = nextApp && nextApp === oldApp; + let alreadyRemoved = removedActors.reduce((result, actor) => { + let removedApp = actor.child._delegate.app; + return result || removedApp === newApp; + }, false); + + if (insertHere || alreadyRemoved) { + let 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++) { + let item = removedActors[i]; + + // Don't animate item removal when the overview is transitioning + // or hidden + if (Main.overview.visible && !Main.overview.animationInProgress) + item.animateOutAndDestroy(); + else + item.destroy(); + } + + this._adjustIconSize(); + + // Skip animations on first run when adding the initial set + // of items, to avoid all items zooming in at once + + let animate = this._shownInitially && Main.overview.visible && + !Main.overview.animationInProgress; + + if (!this._shownInitially) + this._shownInitially = true; + + for (let i = 0; i < addedItems.length; i++) + addedItems[i].item.show(animate); + + // Update separator + const nFavorites = Object.keys(favorites).length; + const nIcons = children.length + addedItems.length - removedActors.length; + if (nFavorites > 0 && nFavorites < nIcons) { + // destroy the horizontal separator if it exists. + // this is incredibly janky, but I can't think of a better way atm. + if (this._separator && this._separator.height !== 1) { + this._separator.destroy(); + this._separator = null; + } + + if (!this._separator) { + this._separator = new St.Widget({ + style_class: 'dash-separator', + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER, + width: opt.DASH_VERTICAL ? this.iconSize : 1, + height: opt.DASH_VERTICAL ? 1 : this.iconSize, + }); + this._box.add_child(this._separator); + } + + // FIXME: separator placement is broken (also in original dash) + let pos = nFavorites + this._animatingPlaceholdersCount; + if (this._dragPlaceholder) + pos++; + this._box.set_child_at_index(this._separator, pos); + } else if (this._separator) { + this._separator.destroy(); + this._separator = null; + } + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 + // Without it, StBoxLayout may use a stale size cache + this._box.queue_relayout(); + }, + + _createAppItem(app) { + let appIcon = new Dash.DashIcon(app); + + let indicator = appIcon._dot; + if (opt.DASH_VERTICAL) { + indicator.x_align = opt.DASH_LEFT ? Clutter.ActorAlign.START : Clutter.ActorAlign.END; + indicator.y_align = Clutter.ActorAlign.CENTER; + } else { + indicator.x_align = Clutter.ActorAlign.CENTER; + indicator.y_align = Clutter.ActorAlign.END; + } + + appIcon.connect('menu-state-changed', + (o, opened) => { + this._itemMenuStateChanged(item, opened); + }); + + let item = new Dash.DashItemContainer(); + item.setChild(appIcon); + + // 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); + + return item; + }, + + // use custom BaseIconSizes and add support for custom icons + _adjustIconSize() { + // if a user launches multiple apps at once, this function may be called again before the previous call has finished + // as a result, new icons will not reach their full size, or will be missing, if adding a new icon and changing the dash size due to lack of space at the same time + if (this._adjustingInProgress) + return; + + // 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) + let iconChildren = this._box.get_children().filter(actor => { + return actor.child && + actor.child._delegate && + actor.child._delegate.icon && + !actor.animatingOut; + }); + + // add new custom icons to the list + if (this._showAppsIcon.visible) + iconChildren.push(this._showAppsIcon); + + + // showWindowsIcon and extensionsIcon can be provided by the WSP and ESP extensions + if (this._showWindowsIcon) + iconChildren.push(this._showWindowsIcon); + + if (this._extensionsIcon) + iconChildren.push(this._extensionsIcon); + + + if (!iconChildren.length) + return; + + if (this._maxWidth === -1 || this._maxHeight === -1) + return; + + const dashHorizontal = !opt.DASH_VERTICAL; + + const themeNode = this.get_theme_node(); + const maxAllocation = new Clutter.ActorBox({ + x1: 0, + y1: 0, + x2: dashHorizontal ? this._maxWidth : 42, // not whatever + y2: dashHorizontal ? 42 : this._maxHeight, + }); + + let maxContent = themeNode.get_content_box(maxAllocation); + + let spacing = themeNode.get_length('spacing'); + + let firstButton = iconChildren[0].child; + let firstIcon = firstButton._delegate.icon; + + if (!firstIcon.icon) + return; + + // Enforce valid spacings during the size request + firstIcon.icon.ensure_style(); + const [, , iconWidth, iconHeight] = firstIcon.icon.get_preferred_size(); + const [, , buttonWidth, buttonHeight] = firstButton.get_preferred_size(); + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + + let maxIconSize = opt.MAX_ICON_SIZE; + if (!maxIconSize) { + maxIconSize = Me.Util.monitorHasLowResolution() + ? 48 + : 64; + } + + let availWidth, availHeight; + if (dashHorizontal) { + availWidth = maxContent.x2 - maxContent.x1; + // Subtract icon padding and box spacing from the available width + availWidth -= iconChildren.length * (buttonWidth - iconWidth) + + (iconChildren.length - 1) * spacing + + 2 * this._background.get_theme_node().get_horizontal_padding(); + + availHeight = this._maxHeight; + availHeight -= this.margin_top + this.margin_bottom; + availHeight -= this._background.get_theme_node().get_vertical_padding(); + availHeight -= themeNode.get_vertical_padding(); + availHeight -= buttonHeight - iconHeight; + + maxIconSize = Math.min(availWidth / iconChildren.length, availHeight, maxIconSize * scaleFactor); + } else { + availWidth = this._maxWidth; + availWidth -= this._background.get_theme_node().get_horizontal_padding(); + availWidth -= themeNode.get_horizontal_padding(); + availWidth -= buttonWidth - iconWidth; + + availHeight = maxContent.y2 - maxContent.y1; + availHeight -= iconChildren.length * (buttonHeight - iconHeight) + + (iconChildren.length - 1) * spacing + + 2 * this._background.get_theme_node().get_vertical_padding(); + + maxIconSize = Math.min(availWidth, availHeight / iconChildren.length, maxIconSize * scaleFactor); + } + + let iconSizes = BaseIconSizes.map(s => s * scaleFactor); + + let newIconSize = BaseIconSizes[0]; + for (let i = 0; i < iconSizes.length; i++) { + if (iconSizes[i] <= maxIconSize) + newIconSize = BaseIconSizes[i]; + } + + if (newIconSize === this.iconSize) + return; + + // set the in-progress state here after all the possible cancels + this._adjustingInProgress = true; + + let oldIconSize = this.iconSize; + this.iconSize = newIconSize; + this.emit('icon-size-changed'); + + let scale = oldIconSize / newIconSize; + for (let i = 0; i < iconChildren.length; i++) { + let icon = iconChildren[i].child._delegate.icon; + + // 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; + + let [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.DASH_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + + if (this._separator) { + this._separator.ease({ + width: dashHorizontal ? 1 : this.iconSize, + height: dashHorizontal ? this.iconSize : 1, + duration: Dash.DASH_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + + this._adjustingInProgress = false; + }, + + handleDragOver(source, actor, x, y, _time) { + let app = getAppFromSource(source); + + // Don't allow favoriting of transient apps + if (app === null || app.is_window_backed()) + return DND.DragMotionResult.NO_DROP; + if (!global.settings.is_writable('favorite-apps')) + return DND.DragMotionResult.NO_DROP; + let favorites = AppFavorites.getAppFavorites().getFavorites(); + let numFavorites = favorites.length; + + let favPos = favorites.indexOf(app); + + let children = this._box.get_children(); + let numChildren = children.length; + let boxSize = opt.DASH_VERTICAL ? this._box.height : this._box.width; + + // Keep the placeholder out of the index calculation; assuming that + // the remove target has the same size as "normal" items, we don't + // need to do the same adjustment there. + if (this._dragPlaceholder) { + boxSize -= opt.DASH_VERTICAL ? this._dragPlaceholder.height : this._dragPlaceholder.width; + numChildren--; + } + + // Same with the separator + if (this._separator) { + boxSize -= opt.DASH_VERTICAL ? this._separator.height : this._separator.width; + numChildren--; + } + + let pos; + if (this._emptyDropTarget) + pos = 0; // always insert at the start when dash is empty + else if (this.text_direction === Clutter.TextDirection.RTL) + pos = numChildren - Math.floor((opt.DASH_VERTICAL ? y : x) * numChildren / boxSize); + else + pos = Math.floor((opt.DASH_VERTICAL ? y : x) * numChildren / boxSize); + + // Put the placeholder after the last favorite if we are not + // in the favorites zone + if (pos > numFavorites) + pos = numFavorites; + + if (pos !== this._dragPlaceholderPos && this._animatingPlaceholdersCount === 0) { + this._dragPlaceholderPos = pos; + + // Don't allow positioning before or after self + if (favPos !== -1 && (pos === favPos || pos === favPos + 1)) { + this._clearDragPlaceholder(); + return DND.DragMotionResult.CONTINUE; + } + + // If the placeholder already exists, we just move + // it, but if we are adding it, expand its size in + // an animation + let fadeIn; + if (this._dragPlaceholder) { + this._dragPlaceholder.destroy(); + fadeIn = false; + } else { + fadeIn = true; + } + + // this._dragPlaceholder = new Dash.DragPlaceholderItem(); // not exported in 45 + this._dragPlaceholder = new Dash.DashItemContainer(); + this._dragPlaceholder.setChild(new St.Bin({ style_class: 'placeholder' })); + this._dragPlaceholder.child.set_width(this.iconSize / (opt.DASH_VERTICAL ? 2 : 1)); + this._dragPlaceholder.child.set_height(this.iconSize / (opt.DASH_VERTICAL ? 1 : 2)); + this._box.insert_child_at_index( + this._dragPlaceholder, + this._dragPlaceholderPos); + this._dragPlaceholder.show(fadeIn); + } + + if (!this._dragPlaceholder) + return DND.DragMotionResult.NO_DROP; + + let srcIsFavorite = favPos !== -1; + + if (srcIsFavorite) + return DND.DragMotionResult.MOVE_DROP; + + return DND.DragMotionResult.COPY_DROP; + }, +}; + +const AppIconCommon = { + after__init() { + if (this._updateRunningDotStyle) + this._updateRunningDotStyle(); + }, + + _updateRunningDotStyle() { + if (opt.RUNNING_DOT_STYLE) + this._dot.add_style_class_name('app-grid-running-dot-custom'); + else + this._dot.remove_style_class_name('app-grid-running-dot-custom'); + }, + + activate(button) { + const event = Clutter.get_current_event(); + const state = event ? event.get_state() : 0; + const isMiddleButton = button && button === Clutter.BUTTON_MIDDLE; + const isCtrlPressed = Me.Util.isCtrlPressed(state); + const isShiftPressed = Me.Util.isShiftPressed(state); + + const currentWS = global.workspace_manager.get_active_workspace(); + const appRecentWorkspace = this._getAppRecentWorkspace(this.app); + // this feature shouldn't affect search results, dash icons don't have labels, so we use them as a condition + const showWidowsBeforeActivation = opt.DASH_CLICK_ACTION === 1 && !this.icon.label; + + let targetWindowOnCurrentWs = false; + if (opt.DASH_FOLLOW_RECENT_WIN) { + targetWindowOnCurrentWs = appRecentWorkspace === currentWS; + } else { + this.app.get_windows().forEach( + w => { + targetWindowOnCurrentWs = targetWindowOnCurrentWs || (w.get_workspace() === currentWS); + } + ); + } + + const openNewWindow = this.app.can_open_new_window() && + this.app.state === Shell.AppState.RUNNING && + (((isCtrlPressed || isMiddleButton) && !opt.DASH_CLICK_OPEN_NEW_WIN) || + (opt.DASH_CLICK_OPEN_NEW_WIN && !this._selectedMetaWin && !isMiddleButton) || + ((opt.DASH_CLICK_PREFER_WORKSPACE || opt.DASH_ISOLATE_WS) && !targetWindowOnCurrentWs)); + + if ((this.app.state === Shell.AppState.STOPPED || openNewWindow) && !isShiftPressed) + this.animateLaunch(); + + if (openNewWindow) { + this.app.open_new_window(-1); + // if DASH_CLICK_ACTION == "SHOW_WINS_BEFORE", the app has more than one window and has no window on the current workspace, + // don't activate the app immediately, only move the overview to the workspace with the app's recent window + } else if (showWidowsBeforeActivation && !isShiftPressed && this.app.get_n_windows() > 1 && !targetWindowOnCurrentWs/* && !(opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE)*/) { + + Main.wm.actionMoveWorkspace(appRecentWorkspace); + Main.overview.dash.showAppsButton.checked = false; + return; + } else if (this._selectedMetaWin) { + this._selectedMetaWin.activate(global.get_current_time()); + } else if (showWidowsBeforeActivation && opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE && !isShiftPressed && this.app.get_n_windows() > 1) { + // expose windows + Main.overview._overview._controls._thumbnailsBox._activateThumbnailAtPoint(0, 0, global.get_current_time(), true); + return; + } else if (((opt.DASH_SHIFT_CLICK_MV && isShiftPressed) || ((opt.DASH_CLICK_PREFER_WORKSPACE || opt.DASH_ISOLATE_WS) && !openNewWindow)) && this.app.get_windows().length) { + this._moveAppToCurrentWorkspace(); + if (opt.DASH_ISOLATE_WS) { + this.app.activate(); + // hide the overview after the window is re-created + GLib.idle_add(GLib.PRIORITY_LOW, () => Main.overview.hide()); + } + return; + } else if (isShiftPressed) { + return; + } else { + this.app.activate(); + } + + Main.overview.hide(); + }, + + _moveAppToCurrentWorkspace() { + this.app.get_windows().forEach(w => w.change_workspace(global.workspace_manager.get_active_workspace())); + }, + + popupMenu(side = St.Side.LEFT) { + this.setForcedHighlight(true); + this._removeMenuTimeout(); + this.fake_release(); + + if (!this._getWindowsOnCurrentWs) { + this._getWindowsOnCurrentWs = function () { + const winList = []; + this.app.get_windows().forEach(w => { + if (w.get_workspace() === global.workspace_manager.get_active_workspace()) + winList.push(w); + }); + return winList; + }; + + this._windowsOnOtherWs = function () { + return (this.app.get_windows().length - this._getWindowsOnCurrentWs().length) > 0; + }; + } + + if (!this._menu) { + this._menu = new AppMenu.AppMenu(this, side, { + favoritesSection: true, + showSingleWindows: true, + }); + + this._menu.setApp(this.app); + this._openSigId = this._menu.connect('open-state-changed', (menu, isPoppedUp) => { + if (!isPoppedUp) + this._onMenuPoppedDown(); + }); + // Main.overview.connectObject('hiding', + this._hidingSigId = Main.overview.connect('hiding', + () => this._menu.close(), this); + + Main.uiGroup.add_child(this._menu.actor); + this._menuManager.addMenu(this._menu); + } + + // once the menu is created, it stays unchanged and we need to modify our items based on current situation + if (this._addedMenuItems && this._addedMenuItems.length) + this._addedMenuItems.forEach(i => i.destroy()); + + + const popupItems = []; + + const separator = new PopupMenu.PopupSeparatorMenuItem(); + this._menu.addMenuItem(separator); + + if (this.app.get_n_windows()) { + // if (/* opt.APP_MENU_FORCE_QUIT*/true) {} + popupItems.push([_('Force Quit'), () => { + this.app.get_windows()[0].kill(); + }]); + + // if (opt.APP_MENU_CLOSE_WS) {} + const nWin = this._getWindowsOnCurrentWs().length; + if (nWin) { + popupItems.push([_(`Close ${nWin} Windows on Current Workspace`), () => { + const windows = this._getWindowsOnCurrentWs(); + let time = global.get_current_time(); + for (let win of windows) { + // increase time by 1 ms for each window to avoid errors from GS + win.delete(time++); + } + }]); + } + + popupItems.push([_('Move App to Current Workspace ( Shift + Click )'), this._moveAppToCurrentWorkspace]); + // WTMB (Windows Thumbnails) extension required + if (global.windowThumbnails) { + popupItems.push([_('Create Window Thumbnail/PiP'), () => { + global.windowThumbnails?.createThumbnail(this.app.get_windows()[0]); + }]); + } + } + + this._addedMenuItems = []; + this._addedMenuItems.push(separator); + popupItems.forEach(i => { + let item = new PopupMenu.PopupMenuItem(i[0]); + this._menu.addMenuItem(item); + item.connect('activate', i[1].bind(this)); + if (i[1] === this._moveAppToCurrentWorkspace && !this._windowsOnOtherWs()) + item.setSensitive(false); + this._addedMenuItems.push(item); + }); + + this.emit('menu-state-changed', true); + + this._menu.open(BoxPointer.PopupAnimation.FULL); + this._menuManager.ignoreRelease(); + this.emit('sync-tooltip'); + + return false; + }, + + _getWindowApp(metaWin) { + const tracker = Shell.WindowTracker.get_default(); + return tracker.get_window_app(metaWin); + }, + + _getAppLastUsedWindow(app) { + let recentWin; + global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).forEach(metaWin => { + const winApp = this._getWindowApp(metaWin); + if (!recentWin && winApp === app) + recentWin = metaWin; + }); + return recentWin; + }, + + _getAppRecentWorkspace(app) { + const recentWin = this._getAppLastUsedWindow(app); + if (recentWin) + return recentWin.get_workspace(); + + return null; + }, +}; + +const DashIconCommon = { + after__init() { + if (opt.DASH_ICON_SCROLL && !Me.Util.dashNotDefault()) { + this._scrollConId = this.connect('scroll-event', DashExtensions.onScrollEvent.bind(this)); + this._leaveConId = this.connect('leave-event', DashExtensions.onLeaveEvent.bind(this)); + } + }, + + popupMenu() { + const side = opt.DASH_VERTICAL ? St.Side.LEFT : St.Side.BOTTOM; + AppIconCommon.popupMenu.bind(this)(side); + }, + + _updateRunningDotStyle() { + if (opt.RUNNING_DOT_STYLE) + this._dot.add_style_class_name('app-grid-running-dot-custom'); + else + this._dot.remove_style_class_name('app-grid-running-dot-custom'); + + this._dot.translation_x = 0; + // _updateDotStyle() has been added in GS 46.2 to apply translation_y value from the CSS on style change + if (shellVersion46 && !this._updateDotStyle && !opt.DASH_VERTICAL) + this._dot.translation_y = 8; + + // GS 46.0 (Ubuntu) only + if (opt.DASH_VERTICAL) + this._dot.translationY = 0; + }, + + _updateRunningStyle() { + const currentWs = global.workspace_manager.get_active_workspace(); + const show = opt.DASH_ISOLATE_WS + ? this.app.get_windows().filter(w => w.get_workspace() === currentWs).length + : this.app.state !== Shell.AppState.STOPPED; + + if (show) + this._dot.show(); + else + this._dot.hide(); + }, +}; + +const DashExtensions = { + onScrollEvent(source, event) { + if ((this.app && !opt.DASH_ICON_SCROLL) || (this._isSearchWindowsIcon && !opt.SEARCH_WINDOWS_ICON_SCROLL)) { + if (this._scrollConId) { + this.disconnect(this._scrollConId); + this._scrollConId = 0; + } + if (this._leaveConId) { + this.disconnect(this._leaveConId); + this._leaveConId = 0; + } + return Clutter.EVENT_PROPAGATE; + } + + if (Main.overview._overview.controls._stateAdjustment.value > 1) + return Clutter.EVENT_PROPAGATE; + + let direction = Me.Util.getScrollDirection(event); + if (direction === Clutter.ScrollDirection.UP) + direction = 1; + else if (direction === Clutter.ScrollDirection.DOWN) + direction = -1; + else + return Clutter.EVENT_STOP; + + // avoid uncontrollable switching if smooth scroll wheel or trackpad is used + if (this._lastScroll && Date.now() - this._lastScroll < 160) + return Clutter.EVENT_STOP; + + this._lastScroll = Date.now(); + + DashExtensions.switchWindow.bind(this)(direction); + return Clutter.EVENT_STOP; + }, + + onLeaveEvent() { + if (!this._selectedMetaWin || this.has_pointer || this.toggleButton?.has_pointer) + return; + + this._selectedPreview._activateSelected = false; + this._selectedMetaWin = null; + this._scrolledWindows = null; + DashExtensions.showWindowPreview.bind(this)(null); + }, + + + switchWindow(direction) { + if (!this._scrolledWindows) { + this._initialSelection = true; + // source is app icon + if (this.app) { + this._scrolledWindows = this.app.get_windows(); + if (opt.DASH_ISOLATE_WS) { + const currentWs = global.workspaceManager.get_active_workspace(); + this._scrolledWindows = this._scrolledWindows.filter(w => w.get_workspace() === currentWs); + } + + const wsList = []; + this._scrolledWindows.forEach(w => { + const ws = w.get_workspace(); + if (!wsList.includes(ws)) + wsList.push(ws); + }); + + // sort windows by workspaces in MRU order + this._scrolledWindows.sort((a, b) => wsList.indexOf(a.get_workspace()) > wsList.indexOf(b.get_workspace())); + // source is Search Windows icon + } else if (this._isSearchWindowsIcon) { + if (opt.SEARCH_WINDOWS_ICON_SCROLL === 1) // all windows + this._scrolledWindows = Me.Util.getWindows(null); + else + this._scrolledWindows = Me.Util.getWindows(global.workspace_manager.get_active_workspace()); + } + } + + let windows = this._scrolledWindows; + + if (!windows.length) + return; + + // if window selection is in the process, the previewed window must be the current one + let currentWin = this._selectedMetaWin ? this._selectedMetaWin : windows[0]; + + const currentIdx = windows.indexOf(currentWin); + let targetIdx = currentIdx; + // const focusWindow = Me.Util.getWindows(null)[0]; // incompatible 45 + const focusWindow = Me.Util.getWindows(null)[0]; + const appFocused = this._scrolledWindows[0] === focusWindow && this._scrolledWindows[0].get_workspace() === global.workspace_manager.get_active_workspace(); + // only if the app has focus, immediately switch to the previous window + // otherwise just set the current window above others + if (!this._initialSelection || appFocused) + targetIdx += direction; + else + this._initialSelection = false; + + if (targetIdx > windows.length - 1) + targetIdx = 0; + else if (targetIdx < 0) + targetIdx = windows.length - 1; + + const metaWin = windows[targetIdx]; + DashExtensions.showWindowPreview.bind(this)(metaWin); + this._selectedMetaWin = metaWin; + }, + + showWindowPreview(metaWin) { + const views = Main.overview._overview.controls._workspacesDisplay._workspacesViews; + const viewsIter = [views[0]]; + // secondary monitors use different structure + views.forEach(v => { + if (v._workspacesView) + viewsIter.push(v._workspacesView); + }); + + viewsIter.forEach(view => { + // if workspaces are on primary monitor only + if (!view || !view._workspaces) + return; + + view._workspaces.forEach(ws => { + ws._windows.forEach(windowPreview => { + // metaWin === null resets opacity + let opacity = metaWin ? 50 : 255; + windowPreview._activateSelected = false; + + // minimized windows are invisible if windows are not exposed (WORKSPACE_MODE === 0) + if (!windowPreview.opacity) + windowPreview.opacity = 255; + + // app windows set to lower opacity, so they can be recognized + if (this._scrolledWindows && this._scrolledWindows.includes(windowPreview.metaWindow)) { + if (opt.DASH_ICON_SCROLL === 2) + opacity = 254; + } + if (windowPreview.metaWindow === metaWin) { + if (metaWin && metaWin.get_workspace() !== global.workspace_manager.get_active_workspace()) { + Main.wm.actionMoveWorkspace(metaWin.get_workspace()); + if (_timeouts.wsSwitcherAnimation) + GLib.source_remove(_timeouts.wsSwitcherAnimation); + // setting window preview above siblings before workspace switcher animation has no effect + // we need to set the window above after the ws preview become visible on the screen + // the default switcher animation time is 250, 200 ms delay should be enough + _timeouts.wsSwitcherAnimation = GLib.timeout_add(0, 200 * St.Settings.get().slow_down_factor, () => { + windowPreview.get_parent().set_child_above_sibling(windowPreview, null); + _timeouts.wsSwitcherAnimation = 0; + return GLib.SOURCE_REMOVE; + }); + } else { + windowPreview.get_parent().set_child_above_sibling(windowPreview, null); + } + + opacity = 255; + this._selectedPreview = windowPreview; + windowPreview._activateSelected = true; + } + + // if windows are exposed, highlight selected using opacity + if ((opt.OVERVIEW_MODE && opt.WORKSPACE_MODE) || !opt.OVERVIEW_MODE) { + if (metaWin && opacity === 255) + windowPreview.showOverlay(true); + else + windowPreview.hideOverlay(true); + windowPreview.ease({ + duration: 200, + opacity, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + }); + }); + }); + }, +}; + +const AppMenuCommon = { + _updateWindowsSection() { + if (global.compositor) { + if (this._updateWindowsLaterId) { + const laters = global.compositor.get_laters(); + laters.remove(this._updateWindowsLaterId); + } + } else if (this._updateWindowsLaterId) { + Meta.later_remove(this._updateWindowsLaterId); + } + + this._updateWindowsLaterId = 0; + + this._windowSection.removeAll(); + this._openWindowsHeader.hide(); + + if (!this._app) + return; + + const minWindows = this._showSingleWindows ? 1 : 2; + const currentWs = global.workspaceManager.get_active_workspace(); + const isolateWs = opt.DASH_ISOLATE_WS && !Main.overview.dash.showAppsButton.checked; + const windows = this._app.get_windows().filter(w => !w.skip_taskbar && (isolateWs ? w.get_workspace() === currentWs : true)); + if (windows.length < minWindows) + return; + + this._openWindowsHeader.show(); + + windows.forEach(window => { + const title = window.title || this._app.get_name(); + const item = this._windowSection.addAction(title, event => { + Main.activateWindow(window, event.get_time()); + }); + window.connectObject('notify::title', () => { + item.label.text = window.title || this._app.get_name(); + }, item); + }); + }, +}; |