7095ef05e1
The WorkspacesView may be scheduled to be destroyed during relayout, and despite that go through the allocate() vfunc. When that happens, the ::destroy handler is called early, so the WorkspacesView clears this._workspaces in result. When vfunc_allocate() is called, various paths rely on the workspaces array being still populated, which is not the case. The existing check at vfunc_allocate() for the WorkspacesView having no children is insufficient, since children are destroyed as a result, not beforehand. This results in the following warnings when going out of overview: JS ERROR: TypeError: workspace is undefined _getSpacing@resource:///org/gnome/shell/ui/workspacesView.js:218:13 vfunc_allocate@resource:///org/gnome/shell/ui/workspacesView.js:344:18 vfunc_allocate@resource:///org/gnome/shell/ui/overviewControls.js:223:33 removeWindow@resource:///org/gnome/shell/ui/workspace.js:856:29 addWindow/<.destroyId<@resource:///org/gnome/shell/ui/workspace.js:808:22 _updateWorkspacesViews@resource:///org/gnome/shell/ui/workspacesView.js:1023:38 prepareToEnterOverview@resource:///org/gnome/shell/ui/workspacesView.js:990:14 prepareToEnterOverview@resource:///org/gnome/shell/ui/overviewControls.js:740:33 gestureBegin@resource:///org/gnome/shell/ui/overviewControls.js:802:14 _gestureBegin@resource:///org/gnome/shell/ui/overview.js:409:33 _beginGesture@resource:///org/gnome/shell/ui/swipeTracker.js:601:14 _handleEvent@resource:///org/gnome/shell/ui/swipeTracker.js:173:26 @resource:///org/gnome/shell/ui/init.js:21:20 This always happens through the _updateWorkspacesViews() paths, so there is a new WorkspacesView taking over and the one being destroyed should silently go away. Related: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6935 Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2918>
1154 lines
37 KiB
JavaScript
1154 lines
37 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
import Clutter from 'gi://Clutter';
|
|
import Gio from 'gi://Gio';
|
|
import GObject from 'gi://GObject';
|
|
import Meta from 'gi://Meta';
|
|
import Shell from 'gi://Shell';
|
|
import St from 'gi://St';
|
|
|
|
import * as Layout from './layout.js';
|
|
|
|
import * as Main from './main.js';
|
|
import * as OverviewControls from './overviewControls.js';
|
|
import * as SwipeTracker from './swipeTracker.js';
|
|
import * as Util from '../misc/util.js';
|
|
import * as Workspace from './workspace.js';
|
|
import {ThumbnailsBox, MAX_THUMBNAIL_SCALE} from './workspaceThumbnail.js';
|
|
|
|
const WORKSPACE_SWITCH_TIME = 250;
|
|
|
|
const MUTTER_SCHEMA = 'org.gnome.mutter';
|
|
|
|
const WORKSPACE_MIN_SPACING = 24;
|
|
const WORKSPACE_MAX_SPACING = 80;
|
|
|
|
const WORKSPACE_INACTIVE_SCALE = 0.94;
|
|
|
|
const SECONDARY_WORKSPACE_SCALE = 0.80;
|
|
|
|
const WorkspacesViewBase = GObject.registerClass({
|
|
GTypeFlags: GObject.TypeFlags.ABSTRACT,
|
|
}, class WorkspacesViewBase extends St.Widget {
|
|
_init(monitorIndex, overviewAdjustment) {
|
|
super._init({
|
|
style_class: 'workspaces-view',
|
|
x_expand: true,
|
|
y_expand: true,
|
|
});
|
|
this.connect('destroy', this._onDestroy.bind(this));
|
|
global.focus_manager.add_group(this);
|
|
|
|
this._monitorIndex = monitorIndex;
|
|
|
|
this._inDrag = false;
|
|
Main.overview.connectObject(
|
|
'window-drag-begin', this._dragBegin.bind(this),
|
|
'window-drag-end', this._dragEnd.bind(this), this);
|
|
|
|
this._overviewAdjustment = overviewAdjustment;
|
|
overviewAdjustment.connectObject('notify::value',
|
|
() => this._updateWorkspaceMode(), this);
|
|
}
|
|
|
|
_onDestroy() {
|
|
this._dragEnd();
|
|
}
|
|
|
|
_dragBegin() {
|
|
this._inDrag = true;
|
|
}
|
|
|
|
_dragEnd() {
|
|
this._inDrag = false;
|
|
}
|
|
|
|
_updateWorkspaceMode() {
|
|
}
|
|
|
|
vfunc_allocate(box) {
|
|
this.set_allocation(box);
|
|
|
|
for (const child of this)
|
|
child.allocate_available_size(0, 0, box.get_width(), box.get_height());
|
|
}
|
|
|
|
vfunc_get_preferred_width() {
|
|
return [0, 0];
|
|
}
|
|
|
|
vfunc_get_preferred_height() {
|
|
return [0, 0];
|
|
}
|
|
});
|
|
|
|
/** @enum {number} */
|
|
export const FitMode = {
|
|
SINGLE: 0,
|
|
ALL: 1,
|
|
};
|
|
|
|
export const WorkspacesView = GObject.registerClass(
|
|
class WorkspacesView extends WorkspacesViewBase {
|
|
_init(monitorIndex, controls, scrollAdjustment, fitModeAdjustment, overviewAdjustment) {
|
|
let workspaceManager = global.workspace_manager;
|
|
|
|
super._init(monitorIndex, overviewAdjustment);
|
|
|
|
this._controls = controls;
|
|
this._fitModeAdjustment = fitModeAdjustment;
|
|
this._fitModeAdjustment.connectObject('notify::value', () => {
|
|
this._updateVisibility();
|
|
this._updateWorkspacesState();
|
|
this.queue_relayout();
|
|
}, this);
|
|
|
|
this._animating = false; // tweening
|
|
this._gestureActive = false; // touch(pad) gestures
|
|
|
|
this._scrollAdjustment = scrollAdjustment;
|
|
this._scrollAdjustment.connectObject('notify::value',
|
|
this._onScrollAdjustmentChanged.bind(this), this);
|
|
|
|
this._workspaces = [];
|
|
this._updateWorkspaces();
|
|
workspaceManager.connectObject(
|
|
'notify::n-workspaces', this._updateWorkspaces.bind(this),
|
|
'workspaces-reordered', () => {
|
|
this._workspaces.sort((a, b) => {
|
|
return a.metaWorkspace.index() - b.metaWorkspace.index();
|
|
});
|
|
this._workspaces.forEach(
|
|
(ws, i) => this.set_child_at_index(ws, i));
|
|
}, this);
|
|
|
|
global.window_manager.connectObject('switch-workspace',
|
|
this._activeWorkspaceChanged.bind(this), this);
|
|
}
|
|
|
|
_getFirstFitAllWorkspaceBox(box, spacing, vertical) {
|
|
const {nWorkspaces} = global.workspaceManager;
|
|
const [width, height] = box.get_size();
|
|
const [workspace] = this._workspaces;
|
|
|
|
const fitAllBox = new Clutter.ActorBox();
|
|
|
|
let [x1, y1] = box.get_origin();
|
|
|
|
// Spacing here is not only the space between workspaces, but also the
|
|
// space before the first workspace, and after the last one. This prevents
|
|
// workspaces from touching the edges of the allocation box.
|
|
if (vertical) {
|
|
const availableHeight = height - spacing * (nWorkspaces + 1);
|
|
let workspaceHeight = availableHeight / nWorkspaces;
|
|
let [, workspaceWidth] =
|
|
workspace.get_preferred_width(workspaceHeight);
|
|
|
|
y1 = spacing;
|
|
if (workspaceWidth > width) {
|
|
[, workspaceHeight] = workspace.get_preferred_height(width);
|
|
y1 += Math.max((availableHeight - workspaceHeight * nWorkspaces) / 2, 0);
|
|
}
|
|
|
|
fitAllBox.set_size(width, workspaceHeight);
|
|
} else {
|
|
const availableWidth = width - spacing * (nWorkspaces + 1);
|
|
let workspaceWidth = availableWidth / nWorkspaces;
|
|
let [, workspaceHeight] =
|
|
workspace.get_preferred_height(workspaceWidth);
|
|
|
|
x1 = spacing;
|
|
if (workspaceHeight > height) {
|
|
[, workspaceWidth] = workspace.get_preferred_width(height);
|
|
x1 += Math.max((availableWidth - workspaceWidth * nWorkspaces) / 2, 0);
|
|
}
|
|
|
|
fitAllBox.set_size(workspaceWidth, height);
|
|
}
|
|
|
|
fitAllBox.set_origin(x1, y1);
|
|
|
|
return fitAllBox;
|
|
}
|
|
|
|
_getFirstFitSingleWorkspaceBox(box, spacing, vertical) {
|
|
const [width, height] = box.get_size();
|
|
const [workspace] = this._workspaces;
|
|
|
|
const rtl = this.text_direction === Clutter.TextDirection.RTL;
|
|
const adj = this._scrollAdjustment;
|
|
const currentWorkspace = vertical || !rtl
|
|
? adj.value : adj.upper - adj.value - 1;
|
|
|
|
// Single fit mode implies centered too
|
|
let [x1, y1] = box.get_origin();
|
|
if (vertical) {
|
|
const [, workspaceHeight] = workspace.get_preferred_height(width);
|
|
y1 += (height - workspaceHeight) / 2;
|
|
y1 -= currentWorkspace * (workspaceHeight + spacing);
|
|
} else {
|
|
const [, workspaceWidth] = workspace.get_preferred_width(height);
|
|
x1 += (width - workspaceWidth) / 2;
|
|
x1 -= currentWorkspace * (workspaceWidth + spacing);
|
|
}
|
|
|
|
const fitSingleBox = new Clutter.ActorBox({x1, y1});
|
|
|
|
if (vertical) {
|
|
const [, workspaceHeight] = workspace.get_preferred_height(width);
|
|
fitSingleBox.set_size(width, workspaceHeight);
|
|
} else {
|
|
const [, workspaceWidth] = workspace.get_preferred_width(height);
|
|
fitSingleBox.set_size(workspaceWidth, height);
|
|
}
|
|
|
|
return fitSingleBox;
|
|
}
|
|
|
|
_getSpacing(box, fitMode, vertical) {
|
|
const [width, height] = box.get_size();
|
|
const [workspace] = this._workspaces;
|
|
|
|
let availableSpace;
|
|
let workspaceSize;
|
|
if (vertical) {
|
|
[, workspaceSize] = workspace.get_preferred_height(width);
|
|
availableSpace = (height - workspaceSize) / 2;
|
|
} else {
|
|
[, workspaceSize] = workspace.get_preferred_width(height);
|
|
availableSpace = (width - workspaceSize) / 2;
|
|
}
|
|
|
|
const spacing = (availableSpace - workspaceSize * 0.4) * (1 - fitMode);
|
|
const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
|
|
|
|
return Math.clamp(spacing, WORKSPACE_MIN_SPACING * scaleFactor,
|
|
WORKSPACE_MAX_SPACING * scaleFactor);
|
|
}
|
|
|
|
_getWorkspaceModeForOverviewState(state) {
|
|
const {ControlsState} = OverviewControls;
|
|
|
|
switch (state) {
|
|
case ControlsState.HIDDEN:
|
|
return 0;
|
|
case ControlsState.WINDOW_PICKER:
|
|
return 1;
|
|
case ControlsState.APP_GRID:
|
|
return 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
_updateWorkspacesState() {
|
|
const adj = this._scrollAdjustment;
|
|
const fitMode = this._fitModeAdjustment.value;
|
|
|
|
const {initialState, finalState, progress} =
|
|
this._overviewAdjustment.getStateTransitionParams();
|
|
|
|
const workspaceMode = (1 - fitMode) * Util.lerp(
|
|
this._getWorkspaceModeForOverviewState(initialState),
|
|
this._getWorkspaceModeForOverviewState(finalState),
|
|
progress);
|
|
|
|
// Fade and scale inactive workspaces
|
|
this._workspaces.forEach((w, index) => {
|
|
w.stateAdjustment.value = workspaceMode;
|
|
|
|
const distanceToCurrentWorkspace = Math.abs(adj.value - index);
|
|
|
|
const scaleProgress = 1 - Math.clamp(distanceToCurrentWorkspace, 0, 1);
|
|
|
|
const scale = Util.lerp(WORKSPACE_INACTIVE_SCALE, 1, scaleProgress);
|
|
w.set_scale(scale, scale);
|
|
});
|
|
}
|
|
|
|
_getFitModeForState(state) {
|
|
const {ControlsState} = OverviewControls;
|
|
|
|
switch (state) {
|
|
case ControlsState.HIDDEN:
|
|
case ControlsState.WINDOW_PICKER:
|
|
return FitMode.SINGLE;
|
|
case ControlsState.APP_GRID:
|
|
return FitMode.ALL;
|
|
default:
|
|
return FitMode.SINGLE;
|
|
}
|
|
}
|
|
|
|
_getInitialBoxes(box) {
|
|
const offsetBox = new Clutter.ActorBox();
|
|
offsetBox.set_size(...box.get_size());
|
|
|
|
let fitSingleBox = offsetBox;
|
|
let fitAllBox = offsetBox;
|
|
|
|
const {transitioning, initialState, finalState} =
|
|
this._overviewAdjustment.getStateTransitionParams();
|
|
|
|
const isPrimary = Main.layoutManager.primaryIndex === this._monitorIndex;
|
|
|
|
if (isPrimary && transitioning) {
|
|
const initialFitMode = this._getFitModeForState(initialState);
|
|
const finalFitMode = this._getFitModeForState(finalState);
|
|
|
|
// Only use the relative boxes when the overview is in a state
|
|
// transition, and the corresponding fit modes are different.
|
|
if (initialFitMode !== finalFitMode) {
|
|
const initialBox =
|
|
this._controls.getWorkspacesBoxForState(initialState).copy();
|
|
const finalBox =
|
|
this._controls.getWorkspacesBoxForState(finalState).copy();
|
|
|
|
// Boxes are relative to ControlsManager, transform them;
|
|
// this.apply_relative_transform_to_point(controls,
|
|
// new Graphene.Point3D());
|
|
// would be more correct, but also more expensive
|
|
const [parentOffsetX, parentOffsetY] =
|
|
this.get_parent().allocation.get_origin();
|
|
[initialBox, finalBox].forEach(b => {
|
|
b.set_origin(b.x1 - parentOffsetX, b.y1 - parentOffsetY);
|
|
});
|
|
|
|
if (initialFitMode === FitMode.SINGLE)
|
|
[fitSingleBox, fitAllBox] = [initialBox, finalBox];
|
|
else
|
|
[fitAllBox, fitSingleBox] = [initialBox, finalBox];
|
|
}
|
|
}
|
|
|
|
return [fitSingleBox, fitAllBox];
|
|
}
|
|
|
|
_updateWorkspaceMode() {
|
|
this._updateWorkspacesState();
|
|
}
|
|
|
|
vfunc_allocate(box) {
|
|
this.set_allocation(box);
|
|
|
|
if (this._workspaces.length === 0)
|
|
return;
|
|
|
|
const vertical = global.workspaceManager.layout_rows === -1;
|
|
const rtl = this.text_direction === Clutter.TextDirection.RTL;
|
|
|
|
const fitMode = this._fitModeAdjustment.value;
|
|
|
|
let [fitSingleBox, fitAllBox] = this._getInitialBoxes(box);
|
|
const fitSingleSpacing =
|
|
this._getSpacing(fitSingleBox, FitMode.SINGLE, vertical);
|
|
fitSingleBox =
|
|
this._getFirstFitSingleWorkspaceBox(fitSingleBox, fitSingleSpacing, vertical);
|
|
|
|
const fitAllSpacing =
|
|
this._getSpacing(fitAllBox, FitMode.ALL, vertical);
|
|
fitAllBox =
|
|
this._getFirstFitAllWorkspaceBox(fitAllBox, fitAllSpacing, vertical);
|
|
|
|
// Account for RTL locales by reversing the list
|
|
const workspaces = this._workspaces.slice();
|
|
if (rtl)
|
|
workspaces.reverse();
|
|
|
|
const [fitSingleX1, fitSingleY1] = fitSingleBox.get_origin();
|
|
const [fitSingleWidth, fitSingleHeight] = fitSingleBox.get_size();
|
|
const [fitAllX1, fitAllY1] = fitAllBox.get_origin();
|
|
const [fitAllWidth, fitAllHeight] = fitAllBox.get_size();
|
|
|
|
workspaces.forEach(child => {
|
|
if (fitMode === FitMode.SINGLE)
|
|
box = fitSingleBox;
|
|
else if (fitMode === FitMode.ALL)
|
|
box = fitAllBox;
|
|
else
|
|
box = fitSingleBox.interpolate(fitAllBox, fitMode);
|
|
|
|
child.allocate_align_fill(box, 0.5, 0.5, false, false);
|
|
|
|
if (vertical) {
|
|
fitSingleBox.set_origin(
|
|
fitSingleX1,
|
|
fitSingleBox.y1 + fitSingleHeight + fitSingleSpacing);
|
|
fitAllBox.set_origin(
|
|
fitAllX1,
|
|
fitAllBox.y1 + fitAllHeight + fitAllSpacing);
|
|
} else {
|
|
fitSingleBox.set_origin(
|
|
fitSingleBox.x1 + fitSingleWidth + fitSingleSpacing,
|
|
fitSingleY1);
|
|
fitAllBox.set_origin(
|
|
fitAllBox.x1 + fitAllWidth + fitAllSpacing,
|
|
fitAllY1);
|
|
}
|
|
});
|
|
}
|
|
|
|
getActiveWorkspace() {
|
|
let workspaceManager = global.workspace_manager;
|
|
let active = workspaceManager.get_active_workspace_index();
|
|
return this._workspaces[active];
|
|
}
|
|
|
|
prepareToLeaveOverview() {
|
|
for (let w = 0; w < this._workspaces.length; w++)
|
|
this._workspaces[w].prepareToLeaveOverview();
|
|
}
|
|
|
|
syncStacking(stackIndices) {
|
|
for (let i = 0; i < this._workspaces.length; i++)
|
|
this._workspaces[i].syncStacking(stackIndices);
|
|
}
|
|
|
|
_scrollToActive() {
|
|
const {workspaceManager} = global;
|
|
const active = workspaceManager.get_active_workspace_index();
|
|
|
|
this._animating = true;
|
|
this._updateVisibility();
|
|
|
|
this._scrollAdjustment.remove_transition('value');
|
|
this._scrollAdjustment.ease(active, {
|
|
duration: WORKSPACE_SWITCH_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
|
|
onComplete: () => {
|
|
this._animating = false;
|
|
this._updateVisibility();
|
|
},
|
|
});
|
|
}
|
|
|
|
_updateVisibility() {
|
|
let workspaceManager = global.workspace_manager;
|
|
let active = workspaceManager.get_active_workspace_index();
|
|
|
|
const fitMode = this._fitModeAdjustment.value;
|
|
const singleFitMode = fitMode === FitMode.SINGLE;
|
|
|
|
for (let w = 0; w < this._workspaces.length; w++) {
|
|
let workspace = this._workspaces[w];
|
|
|
|
if (this._animating || this._gestureActive || !singleFitMode)
|
|
workspace.show();
|
|
else
|
|
workspace.visible = Math.abs(w - active) <= 1;
|
|
}
|
|
}
|
|
|
|
_updateWorkspaces() {
|
|
let workspaceManager = global.workspace_manager;
|
|
let newNumWorkspaces = workspaceManager.n_workspaces;
|
|
|
|
for (let j = 0; j < newNumWorkspaces; j++) {
|
|
let metaWorkspace = workspaceManager.get_workspace_by_index(j);
|
|
let workspace;
|
|
|
|
if (j >= this._workspaces.length) { /* added */
|
|
workspace = new Workspace.Workspace(
|
|
metaWorkspace,
|
|
this._monitorIndex,
|
|
this._overviewAdjustment);
|
|
this.add_actor(workspace);
|
|
this._workspaces[j] = workspace;
|
|
} else {
|
|
workspace = this._workspaces[j];
|
|
|
|
if (workspace.metaWorkspace !== metaWorkspace) { /* removed */
|
|
workspace.destroy();
|
|
this._workspaces.splice(j, 1);
|
|
} /* else kept */
|
|
}
|
|
}
|
|
|
|
for (let j = this._workspaces.length - 1; j >= newNumWorkspaces; j--) {
|
|
this._workspaces[j].destroy();
|
|
this._workspaces.splice(j, 1);
|
|
}
|
|
|
|
this._updateWorkspacesState();
|
|
this._updateVisibility();
|
|
}
|
|
|
|
_activeWorkspaceChanged(_wm, _from, _to, _direction) {
|
|
this._scrollToActive();
|
|
}
|
|
|
|
_onDestroy() {
|
|
super._onDestroy();
|
|
|
|
this._workspaces = [];
|
|
}
|
|
|
|
startTouchGesture() {
|
|
this._gestureActive = true;
|
|
|
|
this._updateVisibility();
|
|
}
|
|
|
|
endTouchGesture() {
|
|
this._gestureActive = false;
|
|
|
|
// Make sure title captions etc are shown as necessary
|
|
this._scrollToActive();
|
|
this._updateVisibility();
|
|
}
|
|
|
|
// sync the workspaces' positions to the value of the scroll adjustment
|
|
// and change the active workspace if appropriate
|
|
_onScrollAdjustmentChanged() {
|
|
if (!this.has_allocation())
|
|
return;
|
|
|
|
const adj = this._scrollAdjustment;
|
|
const allowSwitch =
|
|
adj.get_transition('value') === null && !this._gestureActive;
|
|
|
|
let workspaceManager = global.workspace_manager;
|
|
let active = workspaceManager.get_active_workspace_index();
|
|
let current = Math.round(adj.value);
|
|
|
|
if (allowSwitch && active !== current) {
|
|
if (!this._workspaces[current]) {
|
|
// The current workspace was destroyed. This could happen
|
|
// when you are on the last empty workspace, and consolidate
|
|
// windows using the thumbnail bar.
|
|
// In that case, the intended behavior is to stay on the empty
|
|
// workspace, which is the last one, so pick it.
|
|
current = this._workspaces.length - 1;
|
|
}
|
|
|
|
let metaWorkspace = this._workspaces[current].metaWorkspace;
|
|
metaWorkspace.activate(global.get_current_time());
|
|
}
|
|
|
|
this._updateWorkspacesState();
|
|
this.queue_relayout();
|
|
}
|
|
});
|
|
|
|
export const ExtraWorkspaceView = GObject.registerClass(
|
|
class ExtraWorkspaceView extends WorkspacesViewBase {
|
|
_init(monitorIndex, overviewAdjustment) {
|
|
super._init(monitorIndex, overviewAdjustment);
|
|
this._workspace =
|
|
new Workspace.Workspace(null, monitorIndex, overviewAdjustment);
|
|
this.add_actor(this._workspace);
|
|
}
|
|
|
|
_updateWorkspaceMode() {
|
|
const overviewState = this._overviewAdjustment.value;
|
|
|
|
const progress = Math.clamp(overviewState,
|
|
OverviewControls.ControlsState.HIDDEN,
|
|
OverviewControls.ControlsState.WINDOW_PICKER);
|
|
|
|
this._workspace.stateAdjustment.value = progress;
|
|
}
|
|
|
|
vfunc_allocate(box) {
|
|
this.set_allocation(box);
|
|
|
|
const [width, height] = box.get_size();
|
|
const [, childWidth] = this._workspace.get_preferred_width(height);
|
|
|
|
const childBox = new Clutter.ActorBox();
|
|
childBox.set_origin(Math.round((width - childWidth) / 2), 0);
|
|
childBox.set_size(childWidth, height);
|
|
this._workspace.allocate(childBox);
|
|
}
|
|
|
|
getActiveWorkspace() {
|
|
return this._workspace;
|
|
}
|
|
|
|
prepareToLeaveOverview() {
|
|
this._workspace.prepareToLeaveOverview();
|
|
}
|
|
|
|
syncStacking(stackIndices) {
|
|
this._workspace.syncStacking(stackIndices);
|
|
}
|
|
|
|
startTouchGesture() {
|
|
}
|
|
|
|
endTouchGesture() {
|
|
}
|
|
});
|
|
|
|
export const SecondaryMonitorDisplay = GObject.registerClass(
|
|
class SecondaryMonitorDisplay extends St.Widget {
|
|
_init(monitorIndex, controls, scrollAdjustment, fitModeAdjustment, overviewAdjustment) {
|
|
this._monitorIndex = monitorIndex;
|
|
this._controls = controls;
|
|
this._scrollAdjustment = scrollAdjustment;
|
|
this._fitModeAdjustment = fitModeAdjustment;
|
|
this._overviewAdjustment = overviewAdjustment;
|
|
|
|
super._init({
|
|
style_class: 'secondary-monitor-workspaces',
|
|
constraints: new Layout.MonitorConstraint({
|
|
index: this._monitorIndex,
|
|
work_area: true,
|
|
}),
|
|
clip_to_allocation: true,
|
|
});
|
|
|
|
this.connect('destroy', () => this._onDestroy());
|
|
|
|
this._thumbnails = new ThumbnailsBox(
|
|
this._scrollAdjustment, monitorIndex);
|
|
this.add_child(this._thumbnails);
|
|
|
|
this._thumbnails.connect('notify::should-show',
|
|
() => this._updateThumbnailVisibility());
|
|
|
|
this._overviewAdjustment.connectObject('notify::value', () => {
|
|
this._updateThumbnailParams();
|
|
this.queue_relayout();
|
|
}, this);
|
|
|
|
this._settings = new Gio.Settings({schema_id: MUTTER_SCHEMA});
|
|
this._settings.connect('changed::workspaces-only-on-primary',
|
|
() => this._workspacesOnPrimaryChanged());
|
|
this._workspacesOnPrimaryChanged();
|
|
}
|
|
|
|
_getThumbnailParamsForState(state) {
|
|
const {ControlsState} = OverviewControls;
|
|
|
|
let opacity, scale;
|
|
switch (state) {
|
|
case ControlsState.HIDDEN:
|
|
case ControlsState.WINDOW_PICKER:
|
|
opacity = 255;
|
|
scale = 1;
|
|
break;
|
|
case ControlsState.APP_GRID:
|
|
opacity = 0;
|
|
scale = 0.5;
|
|
break;
|
|
default:
|
|
opacity = 255;
|
|
scale = 1;
|
|
break;
|
|
}
|
|
|
|
return {opacity, scale};
|
|
}
|
|
|
|
_getThumbnailsHeight(box) {
|
|
if (!this._thumbnails.visible)
|
|
return 0;
|
|
|
|
const [width, height] = box.get_size();
|
|
const {expandFraction} = this._thumbnails;
|
|
const [thumbnailsHeight] = this._thumbnails.get_preferred_height(width);
|
|
return Math.min(
|
|
thumbnailsHeight * expandFraction,
|
|
height * MAX_THUMBNAIL_SCALE);
|
|
}
|
|
|
|
_getWorkspacesBoxForState(state, box, padding, thumbnailsHeight, spacing) {
|
|
const {ControlsState} = OverviewControls;
|
|
const workspaceBox = box.copy();
|
|
const [width, height] = workspaceBox.get_size();
|
|
|
|
switch (state) {
|
|
case ControlsState.HIDDEN:
|
|
break;
|
|
case ControlsState.WINDOW_PICKER:
|
|
workspaceBox.set_origin(0, padding + thumbnailsHeight + spacing);
|
|
workspaceBox.set_size(
|
|
width,
|
|
height - 2 * padding - thumbnailsHeight - spacing);
|
|
break;
|
|
case ControlsState.APP_GRID:
|
|
workspaceBox.set_origin(0, padding);
|
|
workspaceBox.set_size(
|
|
width,
|
|
height - 2 * padding);
|
|
break;
|
|
}
|
|
|
|
return workspaceBox;
|
|
}
|
|
|
|
vfunc_allocate(box) {
|
|
this.set_allocation(box);
|
|
|
|
const themeNode = this.get_theme_node();
|
|
const contentBox = themeNode.get_content_box(box);
|
|
const [width, height] = contentBox.get_size();
|
|
const {expandFraction} = this._thumbnails;
|
|
const spacing = themeNode.get_length('spacing') * expandFraction;
|
|
const padding =
|
|
Math.round((1 - SECONDARY_WORKSPACE_SCALE) * height / 2);
|
|
|
|
const thumbnailsHeight = this._getThumbnailsHeight(contentBox);
|
|
|
|
if (this._thumbnails.visible) {
|
|
const childBox = new Clutter.ActorBox();
|
|
childBox.set_origin(0, padding);
|
|
childBox.set_size(width, thumbnailsHeight);
|
|
this._thumbnails.allocate(childBox);
|
|
}
|
|
|
|
const {
|
|
currentState, initialState, finalState, transitioning, progress,
|
|
} = this._overviewAdjustment.getStateTransitionParams();
|
|
|
|
let workspacesBox;
|
|
const workspaceParams = [contentBox, padding, thumbnailsHeight, spacing];
|
|
if (!transitioning) {
|
|
workspacesBox =
|
|
this._getWorkspacesBoxForState(currentState, ...workspaceParams);
|
|
} else {
|
|
const initialBox =
|
|
this._getWorkspacesBoxForState(initialState, ...workspaceParams);
|
|
const finalBox =
|
|
this._getWorkspacesBoxForState(finalState, ...workspaceParams);
|
|
workspacesBox = initialBox.interpolate(finalBox, progress);
|
|
}
|
|
this._workspacesView.allocate(workspacesBox);
|
|
}
|
|
|
|
_onDestroy() {
|
|
if (this._settings)
|
|
this._settings.run_dispose();
|
|
this._settings = null;
|
|
}
|
|
|
|
_workspacesOnPrimaryChanged() {
|
|
this._updateWorkspacesView();
|
|
this._updateThumbnailVisibility();
|
|
}
|
|
|
|
_updateWorkspacesView() {
|
|
if (this._workspacesView)
|
|
this._workspacesView.destroy();
|
|
|
|
if (this._settings.get_boolean('workspaces-only-on-primary')) {
|
|
this._workspacesView = new ExtraWorkspaceView(
|
|
this._monitorIndex,
|
|
this._overviewAdjustment);
|
|
} else {
|
|
this._workspacesView = new WorkspacesView(
|
|
this._monitorIndex,
|
|
this._controls,
|
|
this._scrollAdjustment,
|
|
this._fitModeAdjustment,
|
|
this._overviewAdjustment);
|
|
}
|
|
this.add_child(this._workspacesView);
|
|
}
|
|
|
|
_updateThumbnailVisibility() {
|
|
const visible =
|
|
this._thumbnails.should_show &&
|
|
!this._settings.get_boolean('workspaces-only-on-primary');
|
|
|
|
if (this._thumbnails.visible === visible)
|
|
return;
|
|
|
|
this._thumbnails.show();
|
|
this._updateThumbnailParams();
|
|
this._thumbnails.ease_property('expand-fraction', visible ? 1 : 0, {
|
|
duration: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => (this._thumbnails.visible = visible),
|
|
});
|
|
}
|
|
|
|
_updateThumbnailParams() {
|
|
if (!this._thumbnails.visible)
|
|
return;
|
|
|
|
const {initialState, finalState, progress} =
|
|
this._overviewAdjustment.getStateTransitionParams();
|
|
|
|
const initialParams = this._getThumbnailParamsForState(initialState);
|
|
const finalParams = this._getThumbnailParamsForState(finalState);
|
|
|
|
const opacity =
|
|
Util.lerp(initialParams.opacity, finalParams.opacity, progress);
|
|
const scale =
|
|
Util.lerp(initialParams.scale, finalParams.scale, progress);
|
|
|
|
this._thumbnails.set({
|
|
opacity,
|
|
scale_x: scale,
|
|
scale_y: scale,
|
|
});
|
|
}
|
|
|
|
getActiveWorkspace() {
|
|
return this._workspacesView.getActiveWorkspace();
|
|
}
|
|
|
|
prepareToLeaveOverview() {
|
|
this._workspacesView.prepareToLeaveOverview();
|
|
}
|
|
|
|
syncStacking(stackIndices) {
|
|
this._workspacesView.syncStacking(stackIndices);
|
|
}
|
|
|
|
startTouchGesture() {
|
|
this._workspacesView.startTouchGesture();
|
|
}
|
|
|
|
endTouchGesture() {
|
|
this._workspacesView.endTouchGesture();
|
|
}
|
|
});
|
|
|
|
export const WorkspacesDisplay = GObject.registerClass(
|
|
class WorkspacesDisplay extends St.Widget {
|
|
_init(controls, scrollAdjustment, overviewAdjustment) {
|
|
super._init({
|
|
layout_manager: new Clutter.BinLayout(),
|
|
reactive: true,
|
|
});
|
|
|
|
this._controls = controls;
|
|
this._overviewAdjustment = overviewAdjustment;
|
|
this._fitModeAdjustment = new St.Adjustment({
|
|
actor: this,
|
|
value: FitMode.SINGLE,
|
|
lower: FitMode.SINGLE,
|
|
upper: FitMode.ALL,
|
|
});
|
|
|
|
let workspaceManager = global.workspace_manager;
|
|
this._scrollAdjustment = scrollAdjustment;
|
|
|
|
global.window_manager.connectObject('switch-workspace',
|
|
this._activeWorkspaceChanged.bind(this), this);
|
|
|
|
this._swipeTracker = new SwipeTracker.SwipeTracker(
|
|
Main.layoutManager.overviewGroup,
|
|
Clutter.Orientation.HORIZONTAL,
|
|
Shell.ActionMode.OVERVIEW,
|
|
{allowDrag: false});
|
|
this._swipeTracker.allowLongSwipes = true;
|
|
this._swipeTracker.connect('begin', this._switchWorkspaceBegin.bind(this));
|
|
this._swipeTracker.connect('update', this._switchWorkspaceUpdate.bind(this));
|
|
this._swipeTracker.connect('end', this._switchWorkspaceEnd.bind(this));
|
|
this.connect('notify::mapped', this._updateSwipeTracker.bind(this));
|
|
|
|
workspaceManager.connectObject(
|
|
'workspaces-reordered', this._workspacesReordered.bind(this),
|
|
'notify::layout-rows', this._updateTrackerOrientation.bind(this), this);
|
|
this._updateTrackerOrientation();
|
|
|
|
Main.overview.connectObject(
|
|
'window-drag-begin', this._windowDragBegin.bind(this),
|
|
'window-drag-end', this._windowDragEnd.bind(this), this);
|
|
|
|
this._primaryVisible = true;
|
|
this._primaryIndex = Main.layoutManager.primaryIndex;
|
|
this._workspacesViews = [];
|
|
|
|
this._settings = new Gio.Settings({schema_id: MUTTER_SCHEMA});
|
|
|
|
this._inWindowDrag = false;
|
|
this._leavingOverview = false;
|
|
|
|
this._gestureActive = false; // touch(pad) gestures
|
|
}
|
|
|
|
_windowDragBegin() {
|
|
this._inWindowDrag = true;
|
|
this._updateSwipeTracker();
|
|
}
|
|
|
|
_windowDragEnd() {
|
|
this._inWindowDrag = false;
|
|
this._updateSwipeTracker();
|
|
}
|
|
|
|
_updateSwipeTracker() {
|
|
this._swipeTracker.enabled =
|
|
this.mapped &&
|
|
!this._inWindowDrag &&
|
|
!this._leavingOverview;
|
|
}
|
|
|
|
_workspacesReordered() {
|
|
let workspaceManager = global.workspace_manager;
|
|
|
|
this._scrollAdjustment.value =
|
|
workspaceManager.get_active_workspace_index();
|
|
}
|
|
|
|
_activeWorkspaceChanged(_wm, _from, to, _direction) {
|
|
if (this._gestureActive)
|
|
return;
|
|
|
|
this._scrollAdjustment.ease(to, {
|
|
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
|
|
duration: WORKSPACE_SWITCH_TIME,
|
|
});
|
|
}
|
|
|
|
_updateTrackerOrientation() {
|
|
const {layoutRows} = global.workspace_manager;
|
|
this._swipeTracker.orientation = layoutRows !== -1
|
|
? Clutter.Orientation.HORIZONTAL
|
|
: Clutter.Orientation.VERTICAL;
|
|
}
|
|
|
|
_directionForProgress(progress) {
|
|
if (global.workspace_manager.layout_rows === -1) {
|
|
return progress > 0
|
|
? Meta.MotionDirection.DOWN
|
|
: Meta.MotionDirection.UP;
|
|
} else if (this.text_direction === Clutter.TextDirection.RTL) {
|
|
return progress > 0
|
|
? Meta.MotionDirection.LEFT
|
|
: Meta.MotionDirection.RIGHT;
|
|
} else {
|
|
return progress > 0
|
|
? Meta.MotionDirection.RIGHT
|
|
: Meta.MotionDirection.LEFT;
|
|
}
|
|
}
|
|
|
|
_switchWorkspaceBegin(tracker, monitor) {
|
|
if (this._workspacesOnlyOnPrimary && monitor !== this._primaryIndex)
|
|
return;
|
|
|
|
let workspaceManager = global.workspace_manager;
|
|
let adjustment = this._scrollAdjustment;
|
|
if (this._gestureActive)
|
|
adjustment.remove_transition('value');
|
|
|
|
const distance = global.workspace_manager.layout_rows === -1
|
|
? this.height : this.width;
|
|
|
|
for (let i = 0; i < this._workspacesViews.length; i++)
|
|
this._workspacesViews[i].startTouchGesture();
|
|
|
|
let progress = adjustment.value / adjustment.page_size;
|
|
let points = Array.from(
|
|
{length: workspaceManager.n_workspaces}, (v, i) => i);
|
|
|
|
tracker.confirmSwipe(distance, points, progress, Math.round(progress));
|
|
|
|
this._gestureActive = true;
|
|
}
|
|
|
|
_switchWorkspaceUpdate(tracker, progress) {
|
|
let adjustment = this._scrollAdjustment;
|
|
adjustment.value = progress * adjustment.page_size;
|
|
}
|
|
|
|
_switchWorkspaceEnd(tracker, duration, endProgress) {
|
|
let workspaceManager = global.workspace_manager;
|
|
let newWs = workspaceManager.get_workspace_by_index(endProgress);
|
|
|
|
this._scrollAdjustment.ease(endProgress, {
|
|
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
|
|
duration,
|
|
onComplete: () => {
|
|
if (!newWs.active)
|
|
newWs.activate(global.get_current_time());
|
|
this._endTouchGesture();
|
|
},
|
|
});
|
|
}
|
|
|
|
_endTouchGesture() {
|
|
for (let i = 0; i < this._workspacesViews.length; i++)
|
|
this._workspacesViews[i].endTouchGesture();
|
|
this._gestureActive = false;
|
|
}
|
|
|
|
vfunc_navigate_focus(from, direction) {
|
|
return this._getPrimaryView()?.navigate_focus(from, direction, false);
|
|
}
|
|
|
|
setPrimaryWorkspaceVisible(visible) {
|
|
if (this._primaryVisible === visible)
|
|
return;
|
|
|
|
this._primaryVisible = visible;
|
|
|
|
const primaryIndex = Main.layoutManager.primaryIndex;
|
|
const primaryWorkspace = this._workspacesViews[primaryIndex];
|
|
if (primaryWorkspace)
|
|
primaryWorkspace.visible = visible;
|
|
}
|
|
|
|
prepareToEnterOverview() {
|
|
this.show();
|
|
this._updateWorkspacesViews();
|
|
|
|
Main.overview.connectObject(
|
|
'windows-restacked', this._onRestacked.bind(this),
|
|
'scroll-event', this._onScrollEvent.bind(this), this);
|
|
|
|
global.stage.connectObject(
|
|
'key-press-event', this._onKeyPressEvent.bind(this), this);
|
|
}
|
|
|
|
prepareToLeaveOverview() {
|
|
for (let i = 0; i < this._workspacesViews.length; i++)
|
|
this._workspacesViews[i].prepareToLeaveOverview();
|
|
|
|
this._leavingOverview = true;
|
|
this._updateSwipeTracker();
|
|
}
|
|
|
|
vfunc_hide() {
|
|
Main.overview.disconnectObject(this);
|
|
global.stage.disconnectObject(this);
|
|
|
|
for (let i = 0; i < this._workspacesViews.length; i++)
|
|
this._workspacesViews[i].destroy();
|
|
this._workspacesViews = [];
|
|
|
|
this._leavingOverview = false;
|
|
|
|
super.vfunc_hide();
|
|
}
|
|
|
|
_updateWorkspacesViews() {
|
|
for (let i = 0; i < this._workspacesViews.length; i++)
|
|
this._workspacesViews[i].destroy();
|
|
|
|
this._primaryIndex = Main.layoutManager.primaryIndex;
|
|
this._workspacesViews = [];
|
|
let monitors = Main.layoutManager.monitors;
|
|
for (let i = 0; i < monitors.length; i++) {
|
|
let view;
|
|
if (i === this._primaryIndex) {
|
|
view = new WorkspacesView(i,
|
|
this._controls,
|
|
this._scrollAdjustment,
|
|
this._fitModeAdjustment,
|
|
this._overviewAdjustment);
|
|
|
|
view.visible = this._primaryVisible;
|
|
this.bind_property('opacity', view, 'opacity', GObject.BindingFlags.SYNC_CREATE);
|
|
this.add_child(view);
|
|
} else {
|
|
view = new SecondaryMonitorDisplay(i,
|
|
this._controls,
|
|
this._scrollAdjustment,
|
|
this._fitModeAdjustment,
|
|
this._overviewAdjustment);
|
|
Main.layoutManager.overviewGroup.add_actor(view);
|
|
}
|
|
|
|
this._workspacesViews.push(view);
|
|
}
|
|
}
|
|
|
|
_getMonitorIndexForEvent(event) {
|
|
let [x, y] = event.get_coords();
|
|
let rect = new Meta.Rectangle({x, y, width: 1, height: 1});
|
|
return global.display.get_monitor_index_for_rect(rect);
|
|
}
|
|
|
|
_getPrimaryView() {
|
|
if (!this._workspacesViews.length)
|
|
return null;
|
|
return this._workspacesViews[this._primaryIndex];
|
|
}
|
|
|
|
activeWorkspaceHasMaximizedWindows() {
|
|
const primaryView = this._getPrimaryView();
|
|
return primaryView
|
|
? primaryView.getActiveWorkspace().hasMaximizedWindows()
|
|
: false;
|
|
}
|
|
|
|
_onRestacked(overview, stackIndices) {
|
|
for (let i = 0; i < this._workspacesViews.length; i++)
|
|
this._workspacesViews[i].syncStacking(stackIndices);
|
|
}
|
|
|
|
_onScrollEvent(actor, event) {
|
|
if (this._swipeTracker.canHandleScrollEvent(event))
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
if (!this.mapped)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
if (this._workspacesOnlyOnPrimary &&
|
|
this._getMonitorIndexForEvent(event) !== this._primaryIndex)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
return Main.wm.handleWorkspaceScroll(event);
|
|
}
|
|
|
|
_onKeyPressEvent(actor, event) {
|
|
const {ControlsState} = OverviewControls;
|
|
if (this._overviewAdjustment.value !== ControlsState.WINDOW_PICKER)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
if (!this.reactive)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
const {workspaceManager} = global;
|
|
const vertical = workspaceManager.layout_rows === -1;
|
|
const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
|
|
|
|
let which;
|
|
switch (event.get_key_symbol()) {
|
|
case Clutter.KEY_Page_Up:
|
|
if (vertical)
|
|
which = Meta.MotionDirection.UP;
|
|
else if (rtl)
|
|
which = Meta.MotionDirection.RIGHT;
|
|
else
|
|
which = Meta.MotionDirection.LEFT;
|
|
break;
|
|
case Clutter.KEY_Page_Down:
|
|
if (vertical)
|
|
which = Meta.MotionDirection.DOWN;
|
|
else if (rtl)
|
|
which = Meta.MotionDirection.LEFT;
|
|
else
|
|
which = Meta.MotionDirection.RIGHT;
|
|
break;
|
|
case Clutter.KEY_Home:
|
|
which = 0;
|
|
break;
|
|
case Clutter.KEY_End:
|
|
which = workspaceManager.n_workspaces - 1;
|
|
break;
|
|
default:
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
|
|
let ws;
|
|
if (which < 0)
|
|
// Negative workspace numbers are directions
|
|
// with respect to the current workspace
|
|
ws = workspaceManager.get_active_workspace().get_neighbor(which);
|
|
else
|
|
// Otherwise it is a workspace index
|
|
ws = workspaceManager.get_workspace_by_index(which);
|
|
|
|
if (ws)
|
|
Main.wm.actionMoveWorkspace(ws);
|
|
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
get _workspacesOnlyOnPrimary() {
|
|
return this._settings.get_boolean('workspaces-only-on-primary');
|
|
}
|
|
|
|
get fitModeAdjustment() {
|
|
return this._fitModeAdjustment;
|
|
}
|
|
});
|