From a7253052777df3bcf4b2abe9367de244cbc35da1 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 08:15:40 +0200 Subject: Adding upstream version 20240414. Signed-off-by: Daniel Baumann --- .../vertical-workspaces/lib/workspaceThumbnail.js | 1236 ++++++++++++++++++++ 1 file changed, 1236 insertions(+) create mode 100644 extensions/46/vertical-workspaces/lib/workspaceThumbnail.js (limited to 'extensions/46/vertical-workspaces/lib/workspaceThumbnail.js') diff --git a/extensions/46/vertical-workspaces/lib/workspaceThumbnail.js b/extensions/46/vertical-workspaces/lib/workspaceThumbnail.js new file mode 100644 index 0000000..d64fda2 --- /dev/null +++ b/extensions/46/vertical-workspaces/lib/workspaceThumbnail.js @@ -0,0 +1,1236 @@ +/** + * V-Shell (Vertical Workspaces) + * workspaceThumbnail.js + * + * @author GdH + * @copyright 2022 - 2024 + * @license GPL-3.0 + * + */ + +'use strict'; + +import GLib from 'gi://GLib'; +import Clutter from 'gi://Clutter'; +import St from 'gi://St'; +import Meta from 'gi://Meta'; +import Shell from 'gi://Shell'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as DND from 'resource:///org/gnome/shell/ui/dnd.js'; +import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js'; +import * as OverviewControls from 'resource:///org/gnome/shell/ui/overviewControls.js'; +import * as WorkspaceThumbnail from 'resource:///org/gnome/shell/ui/workspaceThumbnail.js'; +import * as Background from 'resource:///org/gnome/shell/ui/background.js'; + +let Me; +let opt; + +const ThumbnailState = { + NEW: 0, + EXPANDING: 1, + EXPANDED: 2, + ANIMATING_IN: 3, + NORMAL: 4, + REMOVING: 5, + ANIMATING_OUT: 6, + ANIMATED_OUT: 7, + COLLAPSING: 8, + DESTROYED: 9, +}; + +const ControlsState = OverviewControls.ControlsState; + +const WORKSPACE_CUT_SIZE = 10; +const WORKSPACE_KEEP_ALIVE_TIME = 100; + +export const WorkspaceThumbnailModule = class { + constructor(me) { + Me = me; + opt = Me.opt; + + this._firstActivation = true; + this.moduleEnabled = false; + this._overrides = null; + } + + cleanGlobals() { + Me = null; + opt = null; + } + + update(reset) { + this.moduleEnabled = true; + const conflict = false; + + reset = reset || !this.moduleEnabled || 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(' WorkspaceThumbnailModule - Keeping untouched'); + } + + _activateModule() { + if (!this._overrides) + this._overrides = new Me.Util.Overrides(); + + // don't limit max thumbnail scale for other clients than overview, specifically AATWS. + // this variable is not yet implemented in 45.beta.1 + + this._overrides.addOverride('WorkspaceThumbnail', WorkspaceThumbnail.WorkspaceThumbnail.prototype, WorkspaceThumbnailCommon); + this._overrides.addOverride('ThumbnailsBoxCommon', WorkspaceThumbnail.ThumbnailsBox.prototype, ThumbnailsBoxCommon); + + // replacing opt.ORIENTATION local constant with boxOrientation internal variable allows external customers such as the AATWS extension to control the box orientation. + Main.overview._overview.controls._thumbnailsBox._boxOrientation = opt.ORIENTATION; + + console.debug(' WorkspaceThumbnailModule - Activated'); + } + + _disableModule() { + if (this._overrides) + this._overrides.removeAll(); + this._overrides = null; + + console.debug(' WorkspaceThumbnailModule - Disabled'); + } +}; + +const WorkspaceThumbnailCommon = { + // injection to _init() + after__init() { + // layout manager allows aligning widget children + this.layout_manager = new Clutter.BinLayout(); + // adding layout manager to tmb widget breaks wallpaper background aligning and rounded corners + // unless border is removed + if (opt.SHOW_WS_TMB_BG) + this.add_style_class_name('ws-tmb-labeled'); + else + this.add_style_class_name('ws-tmb-transparent'); + + // add workspace thumbnails labels if enabled + if (opt.SHOW_WST_LABELS) { // 0 - disable + const getLabel = function () { + const wsIndex = this.metaWorkspace.index(); + let label = `${wsIndex + 1}`; + if (opt.SHOW_WST_LABELS === 2) { // 2 - index + workspace name + const settings = Me.getSettings('org.gnome.desktop.wm.preferences'); + const wsLabels = settings.get_strv('workspace-names'); + if (wsLabels.length > wsIndex && wsLabels[wsIndex]) + label += `: ${wsLabels[wsIndex]}`; + } else if (opt.SHOW_WST_LABELS === 3) { // 3- index + app name + // global.display.get_tab_list offers workspace filtering using the second argument, but... + // ... it sometimes includes windows from other workspaces, like minimized VBox machines, after Shell restarts + const metaWin = global.display.get_tab_list(0, null).filter( + w => w.get_monitor() === this.monitorIndex && w.get_workspace().index() === wsIndex)[0]; + + if (metaWin) { + const tracker = Shell.WindowTracker.get_default(); + const app = tracker.get_window_app(metaWin); + label += `: ${app ? app.get_name() : ''}`; + } + } else if (opt.SHOW_WST_LABELS === 4) { + const metaWin = global.display.get_tab_list(0, null).filter( + w => w.get_monitor() === this.monitorIndex && w.get_workspace().index() === wsIndex)[0]; + + if (metaWin) + label += `: ${metaWin.title}`; + } + return label; + }.bind(this); + + const label = getLabel(); + + this._wsLabel = new St.Label({ + text: label, + style_class: 'ws-tmb-label', + x_align: Clutter.ActorAlign.FILL, + y_align: Clutter.ActorAlign.END, + x_expand: true, + y_expand: true, + }); + + this._wsLabel._maxOpacity = 255; + this._wsLabel.opacity = this._wsLabel._maxOpacity; + + this.add_child(this._wsLabel); + this.set_child_above_sibling(this._wsLabel, null); + + this._wsIndexConId = this.metaWorkspace.connect('notify::workspace-index', () => { + const newLabel = getLabel(); + this._wsLabel.text = newLabel; + // avoid possibility of accessing non existing ws + if (this._updateLabelTimeout) { + GLib.source_remove(this._updateLabelTimeout); + this._updateLabelTimeout = 0; + } + }); + this._nWindowsConId = this.metaWorkspace.connect('notify::n-windows', () => { + if (this._updateLabelTimeout) + return; + // wait for new data + this._updateLabelTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => { + const newLabel = getLabel(); + this._wsLabel.text = newLabel; + this._updateLabelTimeout = 0; + return GLib.SOURCE_REMOVE; + }); + }); + } + + if (opt.CLOSE_WS_BUTTON_MODE) { + const closeButton = new St.Icon({ + style_class: 'workspace-close-button', + icon_name: 'window-close-symbolic', + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.START, + x_expand: true, + y_expand: true, + reactive: true, + opacity: 0, + }); + + closeButton.connect('button-release-event', () => { + if (opt.CLOSE_WS_BUTTON_MODE) { + this._closeWorkspace(); + return Clutter.EVENT_STOP; + } else { + return Clutter.EVENT_PROPAGATE; + } + }); + + closeButton.connect('button-press-event', () => { + return Clutter.EVENT_STOP; + }); + + closeButton.connect('enter-event', () => { + closeButton.opacity = 255; + if (!Meta.prefs_get_dynamic_workspaces() || (Meta.prefs_get_dynamic_workspaces() && global.workspace_manager.get_n_workspaces() - 1 !== this.metaWorkspace.index())) { + // color the button red if ready to react on clicks + if (opt.CLOSE_WS_BUTTON_MODE < 3 || (opt.CLOSE_WS_BUTTON_MODE === 3 && Me.Util.isCtrlPressed())) + closeButton.add_style_class_name('workspace-close-button-hover'); + } + }); + + closeButton.connect('leave-event', () => { + closeButton.remove_style_class_name('workspace-close-button-hover'); + }); + + this.add_child(closeButton); + this._closeButton = closeButton; + + this.reactive = true; + this._lastCloseClickTime = 0; + } + + if (opt.SHOW_WST_LABELS_ON_HOVER) + this._wsLabel.opacity = 0; + + this.connect('enter-event', () => { + if (opt.CLOSE_WS_BUTTON_MODE && (!Meta.prefs_get_dynamic_workspaces() || (Meta.prefs_get_dynamic_workspaces() && global.workspace_manager.get_n_workspaces() - 1 !== this.metaWorkspace.index()))) + this._closeButton.opacity = 200; + if (opt.SHOW_WST_LABELS_ON_HOVER) { + this._wsLabel.ease({ + duration: 100, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + opacity: this._wsLabel._maxOpacity, + }); + } + }); + + this.connect('leave-event', () => { + this._closeButton.opacity = 0; + if (opt.SHOW_WST_LABELS_ON_HOVER) { + this._wsLabel.ease({ + duration: 100, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + opacity: 0, + }); + } + }); + + if (opt.SHOW_WS_TMB_BG) { + this._bgManager = new Background.BackgroundManager({ + monitorIndex: this.monitorIndex, + container: this._viewport, + vignette: false, + controlPosition: false, + }); + + this._viewport.set_child_below_sibling(this._bgManager.backgroundActor, null); + + // full brightness of the thumbnail bg draws unnecessary attention + // there is a grey bg under the wallpaper + this._bgManager.backgroundActor.opacity = 220; + } + + this.connect('destroy', () => { + if (this._wsIndexConId) + this.metaWorkspace.disconnect(this._wsIndexConId); + + if (this._nWindowsConId) + this.metaWorkspace.disconnect(this._nWindowsConId); + + if (this._updateLabelTimeout) + GLib.source_remove(this._updateLabelTimeout); + + if (this._bgManager) + this._bgManager.destroy(); + }); + }, + + _closeWorkspace() { + // CLOSE_WS_BUTTON_MODE 1: single click, 2: double-click, 3: Ctrl + + if (opt.CLOSE_WS_BUTTON_MODE === 2) { + const doubleClickTime = Clutter.Settings.get_default().double_click_time; + const clickDelay = Date.now() - this._lastCloseClickTime; + if (clickDelay > doubleClickTime) { + this._lastCloseClickTime = Date.now(); + return; + } + } else if (opt.CLOSE_WS_BUTTON_MODE === 3 && !Me.Util.isCtrlPressed()) { + return; + } + + // close windows on this monitor + const windows = global.display.get_tab_list(0, null).filter( + w => w.get_monitor() === this.monitorIndex && w.get_workspace() === this.metaWorkspace + ); + + for (let i = 0; i < windows.length; i++) { + if (!windows[i].is_on_all_workspaces()) + windows[i].delete(global.get_current_time() + i); + } + }, + + activate(time) { + if (this.state > ThumbnailState.NORMAL) + return; + + // if Static Workspace overview mode active, a click on the already active workspace should activate the window picker mode + const wsIndex = this.metaWorkspace.index(); + const lastWsIndex = global.display.get_workspace_manager().get_n_workspaces() - 1; + const stateAdjustment = Main.overview._overview.controls._stateAdjustment; + + if (stateAdjustment.value === ControlsState.APP_GRID) { + if (this.metaWorkspace.active) { + Main.overview._overview.controls._shiftState(Meta.MotionDirection.DOWN); + // if searchActive, hide it immediately + Main.overview.searchEntry.set_text(''); + } else { + this.metaWorkspace.activate(time); + } + } else if (opt.OVERVIEW_MODE2 && !opt.WORKSPACE_MODE && wsIndex < lastWsIndex) { + if (stateAdjustment.value > 1) + stateAdjustment.value = 1; + + + // spread windows + // in OVERVIEW MODE 2 windows are not spread and workspace is not scaled + // we need to repeat transition to the overview state 1 (window picker), but with spreading windows animation + if (this.metaWorkspace.active) { + Main.overview.searchController._setSearchActive(false); + opt.WORKSPACE_MODE = 1; + // setting value to 0 would reset WORKSPACE_MODE + stateAdjustment.value = 0.01; + stateAdjustment.ease(1, { + duration: 200, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } else { + // switch ws + this.metaWorkspace.activate(time); + } + // a click on the current workspace should go back to the main view + } else if (this.metaWorkspace.active) { + Main.overview.hide(); + } else { + this.metaWorkspace.activate(time); + } + }, + + // Draggable target interface used only by ThumbnailsBox + handleDragOverInternal(source, actor, time) { + if (source === Main.xdndHandler) { + this.metaWorkspace.activate(time); + return DND.DragMotionResult.CONTINUE; + } + + if (this.state > ThumbnailState.NORMAL) + return DND.DragMotionResult.CONTINUE; + + if (source.metaWindow && + !this._isMyWindow(source.metaWindow.get_compositor_private())) + return DND.DragMotionResult.MOVE_DROP; + if (source.app && source.app.can_open_new_window()) + return DND.DragMotionResult.COPY_DROP; + if (!source.app && source.shellWorkspaceLaunch) + return DND.DragMotionResult.COPY_DROP; + + if (source instanceof AppDisplay.FolderIcon) + return DND.DragMotionResult.COPY_DROP; + + + return DND.DragMotionResult.CONTINUE; + }, + + acceptDropInternal(source, actor, time) { + if (this.state > ThumbnailState.NORMAL) + return false; + + if (source.metaWindow) { + let win = source.metaWindow.get_compositor_private(); + if (this._isMyWindow(win)) + return false; + + let metaWindow = win.get_meta_window(); + Main.moveWindowToMonitorAndWorkspace(metaWindow, + this.monitorIndex, this.metaWorkspace.index()); + return true; + } else if (source.app && source.app.can_open_new_window()) { + if (source.animateLaunchAtPos) + source.animateLaunchAtPos(actor.x, actor.y); + + source.app.open_new_window(this.metaWorkspace.index()); + return true; + } else if (!source.app && source.shellWorkspaceLaunch) { + // While unused in our own drag sources, shellWorkspaceLaunch allows + // extensions to define custom actions for their drag sources. + source.shellWorkspaceLaunch({ + workspace: this.metaWorkspace.index(), + timestamp: time, + }); + return true; + } else if (source instanceof AppDisplay.FolderIcon) { + for (let app of source.view._apps) { + // const app = Shell.AppSystem.get_default().lookup_app(id); + app.open_new_window(this.metaWorkspace.index()); + } + } + + return false; + }, +}; + +const ThumbnailsBoxCommon = { + after__init(scrollAdjustment, monitorIndex, orientation = opt.ORIENTATION) { + this._boxOrientation = orientation; + }, + + _activateThumbnailAtPoint(stageX, stageY, time, activateCurrent = false) { + if (activateCurrent) { + const thumbnail = this._thumbnails.find(t => t.metaWorkspace.active); + if (thumbnail) + thumbnail.activate(time); + return; + } + const [r_, x, y] = this.transform_stage_point(stageX, stageY); + + let thumbnail; + + if (this._boxOrientation) + thumbnail = this._thumbnails.find(t => y >= t.y && y <= t.y + t.height); + else + thumbnail = this._thumbnails.find(t => x >= t.x && x <= t.x + t.width); + + if (thumbnail) + thumbnail.activate(time); + }, + + acceptDrop(source, actor, x, y, time) { + if (this._dropWorkspace !== -1) { + return this._thumbnails[this._dropWorkspace].acceptDropInternal(source, actor, time); + } else if (this._dropPlaceholderPos !== -1) { + if (!source.metaWindow && + (!source.app || !source.app.can_open_new_window()) && + (source.app || !source.shellWorkspaceLaunch) && + !(source instanceof AppDisplay.FolderIcon)) + return false; + + + let isWindow = !!source.metaWindow; + + let newWorkspaceIndex; + [newWorkspaceIndex, this._dropPlaceholderPos] = [this._dropPlaceholderPos, -1]; + this._spliceIndex = newWorkspaceIndex; + + Main.wm.insertWorkspace(newWorkspaceIndex); + + if (isWindow) { + // Move the window to our monitor first if necessary. + let thumbMonitor = this._thumbnails[newWorkspaceIndex].monitorIndex; + Main.moveWindowToMonitorAndWorkspace(source.metaWindow, + thumbMonitor, newWorkspaceIndex, true); + } else if (source.app && source.app.can_open_new_window()) { + if (source.animateLaunchAtPos) + source.animateLaunchAtPos(actor.x, actor.y); + + source.app.open_new_window(newWorkspaceIndex); + } else if (!source.app && source.shellWorkspaceLaunch) { + // While unused in our own drag sources, shellWorkspaceLaunch allows + // extensions to define custom actions for their drag sources. + source.shellWorkspaceLaunch({ + workspace: newWorkspaceIndex, + timestamp: time, + }); + } else if (source instanceof AppDisplay.FolderIcon) { + for (let app of source.view._apps) { + // const app = Shell.AppSystem.get_default().lookup_app(id); + app.open_new_window(newWorkspaceIndex); + } + } + + if (source.app || (!source.app && source.shellWorkspaceLaunch)) { + // This new workspace will be automatically removed if the application fails + // to open its first window within some time, as tracked by Shell.WindowTracker. + // Here, we only add a very brief timeout to avoid the _immediate_ removal of the + // workspace while we wait for the startup sequence to load. + let workspaceManager = global.workspace_manager; + Main.wm.keepWorkspaceAlive(workspaceManager.get_workspace_by_index(newWorkspaceIndex), + WORKSPACE_KEEP_ALIVE_TIME); + } + + // Start the animation on the workspace (which is actually + // an old one which just became empty) + let thumbnail = this._thumbnails[newWorkspaceIndex]; + this._setThumbnailState(thumbnail, ThumbnailState.NEW); + thumbnail.slide_position = 1; + thumbnail.collapse_fraction = 1; + + this._queueUpdateStates(); + + return true; + } else { + return false; + } + }, + + handleDragOver(source, actor, x, y, time) { + // switch axis for vertical orientation + if (this._boxOrientation) + x = y; + + if (!source.metaWindow && + (!source.app || !source.app.can_open_new_window()) && + (source.app || !source.shellWorkspaceLaunch) && + source !== Main.xdndHandler && !(source instanceof AppDisplay.FolderIcon)) + return DND.DragMotionResult.CONTINUE; + + const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL; + let canCreateWorkspaces = Meta.prefs_get_dynamic_workspaces(); + let spacing = this.get_theme_node().get_length('spacing'); + + this._dropWorkspace = -1; + let placeholderPos = -1; + let length = this._thumbnails.length; + for (let i = 0; i < length; i++) { + const index = rtl ? length - i - 1 : i; + + if (canCreateWorkspaces && source !== Main.xdndHandler) { + const [targetStart, targetEnd] = + this._getPlaceholderTarget(index, spacing, rtl); + + if (x > targetStart && x <= targetEnd) { + placeholderPos = index; + break; + } + } + + if (this._withinWorkspace(x, index, rtl)) { + this._dropWorkspace = index; + break; + } + } + + if (this._dropPlaceholderPos !== placeholderPos) { + this._dropPlaceholderPos = placeholderPos; + this.queue_relayout(); + } + + if (this._dropWorkspace !== -1) + return this._thumbnails[this._dropWorkspace].handleDragOverInternal(source, actor, time); + else if (this._dropPlaceholderPos !== -1) + return source.metaWindow ? DND.DragMotionResult.MOVE_DROP : DND.DragMotionResult.COPY_DROP; + else + return DND.DragMotionResult.CONTINUE; + }, + + _updateStates() { + const controlsManager = Main.overview._overview.controls; + const { currentState } = controlsManager._stateAdjustment.getStateTransitionParams(); + this.SLIDE_ANIMATION_TIME = 200; + this.RESCALE_ANIMATION_TIME = 200; + // remove rescale animation during this scale transition, it is redundant and delayed + if ((currentState < 2 && currentState > 1) || controlsManager._searchController.searchActive) + this.RESCALE_ANIMATION_TIME = 0; + + this._updateStateId = 0; + + // If we are animating the indicator, wait + if (this._animatingIndicator) + return; + + // Likewise if we are in the process of hiding + if (!this._shouldShow && this.visible) + return; + + // Then slide out any thumbnails that have been destroyed + this._iterateStateThumbnails(ThumbnailState.REMOVING, thumbnail => { + this._setThumbnailState(thumbnail, ThumbnailState.ANIMATING_OUT); + + thumbnail.ease_property('slide-position', 1, { + duration: this.SLIDE_ANIMATION_TIME, + mode: Clutter.AnimationMode.LINEAR, + onComplete: () => { + this._setThumbnailState(thumbnail, ThumbnailState.ANIMATED_OUT); + this._queueUpdateStates(); + }, + }); + }); + + // As long as things are sliding out, don't proceed + if (this._stateCounts[ThumbnailState.ANIMATING_OUT] > 0) + return; + + // Once that's complete, we can start scaling to the new size, + // collapse any removed thumbnails and expand added ones + this._iterateStateThumbnails(ThumbnailState.ANIMATED_OUT, thumbnail => { + this._setThumbnailState(thumbnail, ThumbnailState.COLLAPSING); + thumbnail.ease_property('collapse-fraction', 1, { + duration: this.RESCALE_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._stateCounts[thumbnail.state]--; + thumbnail.state = ThumbnailState.DESTROYED; + + let index = this._thumbnails.indexOf(thumbnail); + this._thumbnails.splice(index, 1); + thumbnail.destroy(); + + this._queueUpdateStates(); + }, + }); + }); + + this._iterateStateThumbnails(ThumbnailState.NEW, thumbnail => { + this._setThumbnailState(thumbnail, ThumbnailState.EXPANDING); + thumbnail.ease_property('collapse-fraction', 0, { + duration: this.SLIDE_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._setThumbnailState(thumbnail, ThumbnailState.EXPANDED); + this._queueUpdateStates(); + }, + }); + }); + + if (this._pendingScaleUpdate) { + this.ease_property('scale', this._targetScale, { + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + duration: this.RESCALE_ANIMATION_TIME, + onComplete: () => this._queueUpdateStates(), + }); + this._queueUpdateStates(); + this._pendingScaleUpdate = false; + } + + // Wait until that's done + if (this._scale !== this._targetScale || + this._stateCounts[ThumbnailState.COLLAPSING] > 0 || + this._stateCounts[ThumbnailState.EXPANDING] > 0) + return; + + // And then slide in any new thumbnails + this._iterateStateThumbnails(ThumbnailState.EXPANDED, thumbnail => { + this._setThumbnailState(thumbnail, ThumbnailState.ANIMATING_IN); + thumbnail.ease_property('slide-position', 0, { + duration: this.SLIDE_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._setThumbnailState(thumbnail, ThumbnailState.NORMAL); + }, + }); + }); + }, + + _getPlaceholderTarget(...args) { + if (this._boxOrientation) + return ThumbnailsBoxVertical._getPlaceholderTarget.bind(this)(...args); + else + return ThumbnailsBoxHorizontal._getPlaceholderTarget.bind(this)(...args); + }, + + _withinWorkspace(...args) { + if (this._boxOrientation) + return ThumbnailsBoxVertical._withinWorkspace.bind(this)(...args); + else + return ThumbnailsBoxHorizontal._withinWorkspace.bind(this)(...args); + }, + + vfunc_get_preferred_width(...args) { + if (this._boxOrientation) + return ThumbnailsBoxVertical.vfunc_get_preferred_width.bind(this)(...args); + else + return ThumbnailsBoxHorizontal.vfunc_get_preferred_width.bind(this)(...args); + }, + + vfunc_get_preferred_height(...args) { + if (this._boxOrientation) + return ThumbnailsBoxVertical.vfunc_get_preferred_height.bind(this)(...args); + else + return ThumbnailsBoxHorizontal.vfunc_get_preferred_height.bind(this)(...args); + }, + + vfunc_allocate(...args) { + if (this._boxOrientation) + return ThumbnailsBoxVertical.vfunc_allocate.bind(this)(...args); + else + return ThumbnailsBoxHorizontal.vfunc_allocate.bind(this)(...args); + }, + + _updateShouldShow(...args) { + if (this._boxOrientation) + return ThumbnailsBoxVertical._updateShouldShow.bind(this)(...args); + else + return ThumbnailsBoxHorizontal._updateShouldShow.bind(this)(...args); + }, +}; + +const ThumbnailsBoxVertical = { + _getPlaceholderTarget(index, spacing, rtl) { + this._dropPlaceholder.add_style_class_name('placeholder-vertical'); + const workspace = this._thumbnails[index]; + + let targetY1; + let targetY2; + + if (rtl) { + const baseY = workspace.y + workspace.height; + targetY1 = baseY - WORKSPACE_CUT_SIZE; + targetY2 = baseY + spacing + WORKSPACE_CUT_SIZE; + } else { + targetY1 = workspace.y - spacing - WORKSPACE_CUT_SIZE; + targetY2 = workspace.y + WORKSPACE_CUT_SIZE; + } + + if (index === 0) { + if (rtl) + targetY2 -= spacing + WORKSPACE_CUT_SIZE; + else + targetY1 += spacing + WORKSPACE_CUT_SIZE; + } + + if (index === this._dropPlaceholderPos) { + const placeholderHeight = this._dropPlaceholder.get_height() + spacing; + if (rtl) + targetY2 += placeholderHeight; + else + targetY1 -= placeholderHeight; + } + + return [targetY1, targetY2]; + }, + + _withinWorkspace(y, index, rtl) { + const length = this._thumbnails.length; + const workspace = this._thumbnails[index]; + + let workspaceY1 = workspace.y + WORKSPACE_CUT_SIZE; + let workspaceY2 = workspace.y + workspace.height - WORKSPACE_CUT_SIZE; + + if (index === length - 1) { + if (rtl) + workspaceY1 -= WORKSPACE_CUT_SIZE; + else + workspaceY2 += WORKSPACE_CUT_SIZE; + } + + return y > workspaceY1 && y <= workspaceY2; + }, + + vfunc_get_preferred_width(forHeight) { + if (forHeight < 10) + return [this._porthole.width, this._porthole.width]; + + let themeNode = this.get_theme_node(); + + forHeight = themeNode.adjust_for_width(forHeight); + + let spacing = themeNode.get_length('spacing'); + let nWorkspaces = this._thumbnails.length; + let totalSpacing = (nWorkspaces - 1) * spacing; + + const avail = forHeight - totalSpacing; + + let scale = (avail / nWorkspaces) / this._porthole.height; + + const width = Math.round(this._porthole.width * scale); + return themeNode.adjust_preferred_height(width, width); + }, + + vfunc_get_preferred_height(forWidth) { + if (forWidth < 10) + return [0, this._porthole.height]; + let themeNode = this.get_theme_node(); + + let spacing = themeNode.get_length('spacing'); + let nWorkspaces = this._thumbnails.length; + + // remove also top/bottom box padding + let totalSpacing = (nWorkspaces - 3) * spacing; + + const ratio = this._porthole.width / this._porthole.height; + const tmbHeight = themeNode.adjust_for_width(forWidth) / ratio; + + const naturalheight = this._thumbnails.reduce((accumulator, thumbnail/* , index*/) => { + const progress = 1 - thumbnail.collapse_fraction; + const height = tmbHeight * progress; + return accumulator + height; + }, 0); + return themeNode.adjust_preferred_width(totalSpacing, Math.round(naturalheight)); + }, + + // removes extra space (extraWidth in the original function), we need the box as accurate as possible + // for precise app grid transition animation + vfunc_allocate(box) { + this.set_allocation(box); + + let rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL; + + if (this._thumbnails.length === 0) // not visible + return; + + let themeNode = this.get_theme_node(); + box = themeNode.get_content_box(box); + + const portholeWidth = this._porthole.width; + const portholeHeight = this._porthole.height; + const spacing = themeNode.get_length('spacing'); + + /* const nWorkspaces = this._thumbnails.length;*/ + + // Compute the scale we'll need once everything is updated, + // unless we are currently transitioning + if (this._expandFraction === 1) { + // remove size "breathing" during adding/removing workspaces + + /* const totalSpacing = (nWorkspaces - 1) * spacing; + const availableHeight = (box.get_height() - totalSpacing) / nWorkspaces; */ + + const hScale = box.get_width() / portholeWidth; + /* const vScale = availableHeight / portholeHeight;*/ + const vScale = box.get_height() / portholeHeight; + const newScale = Math.min(hScale, vScale); + + if (newScale !== this._targetScale) { + if (this._targetScale > 0) { + // We don't ease immediately because we need to observe the + // ordering in queueUpdateStates - if workspaces have been + // removed we need to slide them out as the first thing. + this._targetScale = newScale; + this._pendingScaleUpdate = true; + } else { + this._targetScale = this._scale = newScale; + } + + this._queueUpdateStates(); + } + } + + const ratio = portholeWidth / portholeHeight; + const thumbnailFullHeight = Math.round(portholeHeight * this._scale); + const thumbnailWidth = Math.round(thumbnailFullHeight * ratio); + const thumbnailHeight = thumbnailFullHeight * this._expandFraction; + const roundedVScale = thumbnailHeight / portholeHeight; + + let indicatorValue = this._scrollAdjustment.value; + let indicatorUpperWs = Math.ceil(indicatorValue); + let indicatorLowerWs = Math.floor(indicatorValue); + + let indicatorLowerY1 = 0; + let indicatorLowerY2 = 0; + let indicatorUpperY1 = 0; + let indicatorUpperY2 = 0; + + let indicatorThemeNode = this._indicator.get_theme_node(); + let indicatorTopFullBorder = indicatorThemeNode.get_padding(St.Side.TOP) + indicatorThemeNode.get_border_width(St.Side.TOP); + let indicatorBottomFullBorder = indicatorThemeNode.get_padding(St.Side.BOTTOM) + indicatorThemeNode.get_border_width(St.Side.BOTTOM); + let indicatorLeftFullBorder = indicatorThemeNode.get_padding(St.Side.LEFT) + indicatorThemeNode.get_border_width(St.Side.LEFT); + let indicatorRightFullBorder = indicatorThemeNode.get_padding(St.Side.RIGHT) + indicatorThemeNode.get_border_width(St.Side.RIGHT); + + let y = box.y1; + + if (this._dropPlaceholderPos === -1) { + this._dropPlaceholder.allocate_preferred_size( + ...this._dropPlaceholder.get_position()); + + const laters = global.compositor.get_laters(); + laters.add(Meta.LaterType.BEFORE_REDRAW, () => { + this._dropPlaceholder.hide(); + }); + } + + let childBox = new Clutter.ActorBox(); + + for (let i = 0; i < this._thumbnails.length; i++) { + const thumbnail = this._thumbnails[i]; + if (i > 0) + y += spacing - Math.round(thumbnail.collapse_fraction * spacing); + + const x1 = box.x1; + const x2 = x1 + thumbnailWidth; + + if (i === this._dropPlaceholderPos) { + let [, placeholderHeight] = this._dropPlaceholder.get_preferred_width(-1); + childBox.x1 = x1; + childBox.x2 = x2; + + if (rtl) { + childBox.y2 = box.y2 - Math.round(y); + childBox.y1 = box.y2 - Math.round(y + placeholderHeight); + } else { + childBox.y1 = Math.round(y); + childBox.y2 = Math.round(y + placeholderHeight); + } + + this._dropPlaceholder.allocate(childBox); + + const laters = global.compositor.get_laters(); + laters.add(Meta.LaterType.BEFORE_REDRAW, () => { + this._dropPlaceholder.show(); + }); + y += placeholderHeight + spacing; + } + + // We might end up with thumbnailWidth being something like 99.33 + // pixels. To make this work and not end up with a gap at the end, + // we need some thumbnails to be 99 pixels and some 100 pixels width; + // we compute an actual scale separately for each thumbnail. + const y1 = Math.round(y); + const y2 = Math.round(y + thumbnailHeight); + const roundedHScale = (y2 - y1) / portholeHeight; + + // Allocating a scaled actor is funny - x1/y1 correspond to the origin + // of the actor, but x2/y2 are increased by the *unscaled* size. + if (rtl) { + childBox.y2 = box.y2 - y1; + childBox.y1 = box.y2 - (y1 + thumbnailHeight); + } else { + childBox.y1 = y1; + childBox.y2 = y1 + thumbnailHeight; + } + childBox.x1 = x1; + childBox.x2 = x1 + thumbnailWidth; + + thumbnail.setScale(roundedHScale, roundedVScale); + thumbnail.allocate(childBox); + + if (i === indicatorUpperWs) { + indicatorUpperY1 = childBox.y1; + indicatorUpperY2 = childBox.y2; + } + if (i === indicatorLowerWs) { + indicatorLowerY1 = childBox.y1; + indicatorLowerY2 = childBox.y2; + } + + // We round the collapsing portion so that we don't get thumbnails resizing + // during an animation due to differences in rounded, but leave the uncollapsed + // portion unrounded so that non-animating we end up with the right total + y += thumbnailHeight - Math.round(thumbnailHeight * thumbnail.collapse_fraction); + } + + childBox.x1 = box.x1; + childBox.x2 = box.x1 + thumbnailWidth; + + const indicatorY1 = indicatorLowerY1 + + (indicatorUpperY1 - indicatorLowerY1) * (indicatorValue % 1); + const indicatorY2 = indicatorLowerY2 + + (indicatorUpperY2 - indicatorLowerY2) * (indicatorValue % 1); + + childBox.y1 = indicatorY1 - indicatorTopFullBorder; + childBox.y2 = indicatorY2 + indicatorBottomFullBorder; + childBox.x1 -= indicatorLeftFullBorder; + childBox.x2 += indicatorRightFullBorder; + this._indicator.allocate(childBox); + }, + + _updateShouldShow() { + const shouldShow = opt.SHOW_WS_TMB; + if (this._shouldShow === shouldShow) + return; + + this._shouldShow = shouldShow; + this.notify('should-show'); + }, +}; + +// ThumbnailsBox Horizontal + +const ThumbnailsBoxHorizontal = { + _getPlaceholderTarget(index, spacing, rtl) { + const workspace = this._thumbnails[index]; + + let targetX1; + let targetX2; + + if (rtl) { + const baseX = workspace.x + workspace.width; + targetX1 = baseX - WORKSPACE_CUT_SIZE; + targetX2 = baseX + spacing + WORKSPACE_CUT_SIZE; + } else { + targetX1 = workspace.x - spacing - WORKSPACE_CUT_SIZE; + targetX2 = workspace.x + WORKSPACE_CUT_SIZE; + } + + if (index === 0) { + if (rtl) + targetX2 -= spacing + WORKSPACE_CUT_SIZE; + else + targetX1 += spacing + WORKSPACE_CUT_SIZE; + } + + if (index === this._dropPlaceholderPos) { + const placeholderWidth = this._dropPlaceholder.get_width() + spacing; + if (rtl) + targetX2 += placeholderWidth; + else + targetX1 -= placeholderWidth; + } + + return [targetX1, targetX2]; + }, + + _withinWorkspace(x, index, rtl) { + const length = this._thumbnails.length; + const workspace = this._thumbnails[index]; + + let workspaceX1 = workspace.x + WORKSPACE_CUT_SIZE; + let workspaceX2 = workspace.x + workspace.width - WORKSPACE_CUT_SIZE; + + if (index === length - 1) { + if (rtl) + workspaceX1 -= WORKSPACE_CUT_SIZE; + else + workspaceX2 += WORKSPACE_CUT_SIZE; + } + + return x > workspaceX1 && x <= workspaceX2; + }, + + vfunc_get_preferred_height(forWidth) { + if (forWidth < 10) + return [this._porthole.height, this._porthole.height]; + + let themeNode = this.get_theme_node(); + + forWidth = themeNode.adjust_for_width(forWidth); + + let spacing = themeNode.get_length('spacing'); + let nWorkspaces = this._thumbnails.length; + let totalSpacing = (nWorkspaces - 1) * spacing; + + const avail = forWidth - totalSpacing; + + let scale = (avail / nWorkspaces) / this._porthole.width; + + const height = Math.round(this._porthole.height * scale); + + return themeNode.adjust_preferred_height(height, height); + }, + + vfunc_get_preferred_width(forHeight) { + if (forHeight < 10) + return [0, this._porthole.width]; + + let themeNode = this.get_theme_node(); + + let spacing = themeNode.get_length('spacing'); + let nWorkspaces = this._thumbnails.length; + // remove also left/right box padding from the total spacing + let totalSpacing = (nWorkspaces - 3) * spacing; + + const ratio = this._porthole.height / this._porthole.width; + + const tmbWidth = themeNode.adjust_for_height(forHeight) / ratio; + + const naturalWidth = this._thumbnails.reduce((accumulator, thumbnail) => { + const progress = 1 - thumbnail.collapse_fraction; + const width = tmbWidth * progress; + return accumulator + width; + }, 0); + + return themeNode.adjust_preferred_width(totalSpacing, naturalWidth); + }, + + vfunc_allocate(box) { + this.set_allocation(box); + + let rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL; + + if (this._thumbnails.length === 0) // not visible + return; + + let themeNode = this.get_theme_node(); + box = themeNode.get_content_box(box); + + const portholeWidth = this._porthole.width; + const portholeHeight = this._porthole.height; + const spacing = themeNode.get_length('spacing'); + + /* const nWorkspaces = this._thumbnails.length; */ + + // Compute the scale we'll need once everything is updated, + // unless we are currently transitioning + if (this._expandFraction === 1) { + // remove size "breathing" during adding/removing workspaces + + /* const totalSpacing = (nWorkspaces - 1) * spacing; + const availableWidth = (box.get_width() - totalSpacing) / nWorkspaces; + + const hScale = availableWidth / portholeWidth; */ + const hScale = box.get_width() / portholeWidth; + const vScale = box.get_height() / portholeHeight; + const newScale = Math.min(hScale, vScale); + + if (newScale !== this._targetScale) { + if (this._targetScale > 0) { + // We don't ease immediately because we need to observe the + // ordering in queueUpdateStates - if workspaces have been + // removed we need to slide them out as the first thing. + this._targetScale = newScale; + this._pendingScaleUpdate = true; + } else { + this._targetScale = this._scale = newScale; + } + + this._queueUpdateStates(); + } + } + + const ratio = portholeWidth / portholeHeight; + const thumbnailFullHeight = Math.round(portholeHeight * this._scale); + const thumbnailWidth = Math.round(thumbnailFullHeight * ratio); + const thumbnailHeight = thumbnailFullHeight * this._expandFraction; + const roundedVScale = thumbnailHeight / portholeHeight; + + let indicatorValue = this._scrollAdjustment.value; + let indicatorUpperWs = Math.ceil(indicatorValue); + let indicatorLowerWs = Math.floor(indicatorValue); + + let indicatorLowerX1 = 0; + let indicatorLowerX2 = 0; + let indicatorUpperX1 = 0; + let indicatorUpperX2 = 0; + + let indicatorThemeNode = this._indicator.get_theme_node(); + let indicatorTopFullBorder = indicatorThemeNode.get_padding(St.Side.TOP) + indicatorThemeNode.get_border_width(St.Side.TOP); + let indicatorBottomFullBorder = indicatorThemeNode.get_padding(St.Side.BOTTOM) + indicatorThemeNode.get_border_width(St.Side.BOTTOM); + let indicatorLeftFullBorder = indicatorThemeNode.get_padding(St.Side.LEFT) + indicatorThemeNode.get_border_width(St.Side.LEFT); + let indicatorRightFullBorder = indicatorThemeNode.get_padding(St.Side.RIGHT) + indicatorThemeNode.get_border_width(St.Side.RIGHT); + + let x = box.x1; + + if (this._dropPlaceholderPos === -1) { + this._dropPlaceholder.allocate_preferred_size( + ...this._dropPlaceholder.get_position()); + + const laters = global.compositor.get_laters(); + laters.add(Meta.LaterType.BEFORE_REDRAW, () => { + this._dropPlaceholder.hide(); + }); + } + + let childBox = new Clutter.ActorBox(); + + for (let i = 0; i < this._thumbnails.length; i++) { + const thumbnail = this._thumbnails[i]; + if (i > 0) + x += spacing - Math.round(thumbnail.collapse_fraction * spacing); + + const y1 = box.y1; + const y2 = y1 + thumbnailHeight; + + if (i === this._dropPlaceholderPos) { + const [, placeholderWidth] = this._dropPlaceholder.get_preferred_width(-1); + childBox.y1 = y1; + childBox.y2 = y2; + + if (rtl) { + childBox.x2 = box.x2 - Math.round(x); + childBox.x1 = box.x2 - Math.round(x + placeholderWidth); + } else { + childBox.x1 = Math.round(x); + childBox.x2 = Math.round(x + placeholderWidth); + } + + this._dropPlaceholder.allocate(childBox); + + const laters = global.compositor.get_laters(); + laters.add(Meta.LaterType.BEFORE_REDRAW, () => { + this._dropPlaceholder.show(); + }); + x += placeholderWidth + spacing; + } + + // We might end up with thumbnailWidth being something like 99.33 + // pixels. To make this work and not end up with a gap at the end, + // we need some thumbnails to be 99 pixels and some 100 pixels width; + // we compute an actual scale separately for each thumbnail. + const x1 = Math.round(x); + const x2 = Math.round(x + thumbnailWidth); + const roundedHScale = (x2 - x1) / portholeWidth; + + // Allocating a scaled actor is funny - x1/y1 correspond to the origin + // of the actor, but x2/y2 are increased by the *unscaled* size. + if (rtl) { + childBox.x2 = box.x2 - x1; + childBox.x1 = box.x2 - (x1 + thumbnailWidth); + } else { + childBox.x1 = x1; + childBox.x2 = x1 + thumbnailWidth; + } + childBox.y1 = y1; + childBox.y2 = y1 + thumbnailHeight; + + thumbnail.setScale(roundedHScale, roundedVScale); + thumbnail.allocate(childBox); + + if (i === indicatorUpperWs) { + indicatorUpperX1 = childBox.x1; + indicatorUpperX2 = childBox.x2; + } + if (i === indicatorLowerWs) { + indicatorLowerX1 = childBox.x1; + indicatorLowerX2 = childBox.x2; + } + + // We round the collapsing portion so that we don't get thumbnails resizing + // during an animation due to differences in rounded, but leave the uncollapsed + // portion unrounded so that non-animating we end up with the right total + x += thumbnailWidth - Math.round(thumbnailWidth * thumbnail.collapse_fraction); + } + + childBox.y1 = box.y1; + childBox.y2 = box.y1 + thumbnailHeight; + + const indicatorX1 = indicatorLowerX1 + + (indicatorUpperX1 - indicatorLowerX1) * (indicatorValue % 1); + const indicatorX2 = indicatorLowerX2 + + (indicatorUpperX2 - indicatorLowerX2) * (indicatorValue % 1); + + childBox.x1 = indicatorX1 - indicatorLeftFullBorder; + childBox.x2 = indicatorX2 + indicatorRightFullBorder; + childBox.y1 -= indicatorTopFullBorder; + childBox.y2 += indicatorBottomFullBorder; + this._indicator.allocate(childBox); + }, + + _updateShouldShow: ThumbnailsBoxVertical._updateShouldShow, +}; -- cgit v1.2.3