1
0
Fork 0
gnome-shell-extensions-extra/extensions/46/vertical-workspaces/lib/workspaceThumbnail.js
Daniel Baumann 184ff0f365
Merging upstream version 20240414 (Closes: #1067433).
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-09 23:18:13 +01:00

1236 lines
47 KiB
JavaScript

/**
* V-Shell (Vertical Workspaces)
* workspaceThumbnail.js
*
* @author GdH <G-dH@github.com>
* @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,
};