![Alexander Mikhaylenko](/assets/img/avatar_default.png)
Currently, there's one animation for the whole canvas. While it looks fine with just one screen, it causes windows to move between screens when switching workspaces. Instead, have a separate animation on each screen, and sync their progress so that at any given time the progress "fraction" is the same between all screens. Clip all animations to their screens so that the windows don't leak to other screens. If a window is placed between every screen, can end up in multiple animations, in that case each part is still animated separately. Fixes https://gitlab.gnome.org/GNOME/gnome-shell/issues/1213 https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/605
512 lines
16 KiB
JavaScript
512 lines
16 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/* exported WorkspaceAnimationController */
|
|
|
|
const { Clutter, GObject, Meta, Shell } = imports.gi;
|
|
|
|
const Main = imports.ui.main;
|
|
const Layout = imports.ui.layout;
|
|
const SwipeTracker = imports.ui.swipeTracker;
|
|
|
|
const WINDOW_ANIMATION_TIME = 250;
|
|
|
|
const WorkspaceGroup = GObject.registerClass(
|
|
class WorkspaceGroup extends Clutter.Actor {
|
|
_init(controller, workspace, monitor) {
|
|
super._init();
|
|
|
|
this._controller = controller;
|
|
this._workspace = workspace;
|
|
this._monitor = monitor;
|
|
this._windows = [];
|
|
|
|
this._refreshWindows();
|
|
|
|
this.connect('destroy', this._onDestroy.bind(this));
|
|
this._restackedId = global.display.connect('restacked',
|
|
this._refreshWindows.bind(this));
|
|
}
|
|
|
|
_shouldShowWindow(window) {
|
|
if (window.get_workspace() !== this._workspace)
|
|
return false;
|
|
|
|
let geometry = global.display.get_monitor_geometry(this._monitor.index);
|
|
let [intersects, intersection_] = window.get_frame_rect().intersect(geometry);
|
|
if (!intersects)
|
|
return false;
|
|
|
|
if (!window.showing_on_its_workspace())
|
|
return false;
|
|
|
|
if (window.is_on_all_workspaces())
|
|
return false;
|
|
|
|
if (this._controller.movingWindow &&
|
|
window === this._controller.movingWindow)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
_refreshWindows() {
|
|
if (this._windows.length > 0)
|
|
this._removeWindows();
|
|
|
|
let windows = global.get_window_actors();
|
|
windows = windows.filter(w => this._shouldShowWindow(w.meta_window));
|
|
|
|
for (let window of windows) {
|
|
let clone = new Clutter.Clone({
|
|
source: window,
|
|
x: window.x - this._monitor.x,
|
|
y: window.y - this._monitor.y,
|
|
});
|
|
|
|
this.add_actor(clone);
|
|
window.hide();
|
|
|
|
let record = { window, clone };
|
|
|
|
record.windowDestroyId = window.connect('destroy', () => {
|
|
clone.destroy();
|
|
this._windows.splice(this._windows.indexOf(record), 1);
|
|
});
|
|
|
|
this._windows.push(record);
|
|
}
|
|
}
|
|
|
|
_removeWindows() {
|
|
for (let i = 0; i < this._windows.length; i++) {
|
|
let w = this._windows[i];
|
|
|
|
w.window.disconnect(w.windowDestroyId);
|
|
w.clone.destroy();
|
|
|
|
if (w.window.get_meta_window().get_workspace() ===
|
|
global.workspace_manager.get_active_workspace())
|
|
w.window.show();
|
|
}
|
|
|
|
this._windows = [];
|
|
}
|
|
|
|
_onDestroy() {
|
|
global.display.disconnect(this._restackedId);
|
|
this._removeWindows();
|
|
}
|
|
});
|
|
|
|
const WorkspaceAnimation = GObject.registerClass({
|
|
Properties: {
|
|
'progress': GObject.ParamSpec.double(
|
|
'progress', 'progress', 'progress',
|
|
GObject.ParamFlags.READWRITE,
|
|
-1, 1, 0),
|
|
},
|
|
}, class WorkspaceAnimation extends Clutter.Actor {
|
|
_init(controller, from, to, direction) {
|
|
super._init();
|
|
|
|
this.connect('destroy', this._onDestroy.bind(this));
|
|
|
|
this._controller = controller;
|
|
this._movingWindow = null;
|
|
this._monitors = [];
|
|
this._progress = 0;
|
|
|
|
global.window_group.add_actor(this);
|
|
|
|
let workspaceManager = global.workspace_manager;
|
|
let curWs = workspaceManager.get_workspace_by_index(from);
|
|
|
|
for (let monitor of Main.layoutManager.monitors) {
|
|
let record = {
|
|
index: monitor.index,
|
|
clipBin: new Clutter.Actor({
|
|
x_expand: true,
|
|
y_expand: true,
|
|
clip_to_allocation: true,
|
|
}),
|
|
container: new Clutter.Actor(),
|
|
surroundings: {},
|
|
};
|
|
|
|
let constraint = new Layout.MonitorConstraint({ index: monitor.index });
|
|
record.clipBin.add_constraint(constraint);
|
|
|
|
record.clipBin.add_actor(record.container);
|
|
|
|
this.add_actor(record.clipBin);
|
|
|
|
record.curGroup = new WorkspaceGroup(controller, curWs, monitor);
|
|
record.container.add_actor(record.curGroup);
|
|
|
|
for (let dir of Object.values(Meta.MotionDirection)) {
|
|
let ws = null;
|
|
|
|
if (to < 0)
|
|
ws = curWs.get_neighbor(dir);
|
|
else if (dir === direction)
|
|
ws = workspaceManager.get_workspace_by_index(to);
|
|
|
|
if (ws === null || ws === curWs) {
|
|
record.surroundings[dir] = null;
|
|
continue;
|
|
}
|
|
|
|
let [x, y] = this._getPositionForDirection(dir, curWs, ws,
|
|
monitor.index);
|
|
let info = {
|
|
index: ws.index(),
|
|
actor: new WorkspaceGroup(controller, ws, monitor),
|
|
xDest: x,
|
|
yDest: y,
|
|
};
|
|
record.surroundings[dir] = info;
|
|
record.container.add_actor(info.actor);
|
|
record.container.set_child_above_sibling(info.actor, null);
|
|
|
|
info.actor.set_position(x, y);
|
|
}
|
|
|
|
this._monitors.push(record);
|
|
}
|
|
|
|
if (this._controller.movingWindow) {
|
|
let actor = this._controller.movingWindow.get_compositor_private();
|
|
let container = new Clutter.Actor();
|
|
|
|
this._movingWindow = {
|
|
container,
|
|
window: actor,
|
|
parent: actor.get_parent(),
|
|
};
|
|
|
|
this._movingWindow.parent.remove_child(actor);
|
|
this._movingWindow.container.add_child(actor);
|
|
this._movingWindow.windowDestroyId = actor.connect('destroy', () => {
|
|
this._movingWindow = null;
|
|
});
|
|
|
|
global.window_group.add_actor(container);
|
|
global.window_group.set_child_above_sibling(container, null);
|
|
}
|
|
}
|
|
|
|
_onDestroy() {
|
|
this._monitors = [];
|
|
|
|
if (this._movingWindow) {
|
|
let record = this._movingWindow;
|
|
record.window.disconnect(record.windowDestroyId);
|
|
record.window.get_parent().remove_child(record.window);
|
|
record.parent.add_child(record.window);
|
|
record.container.destroy();
|
|
|
|
this._movingWindow = null;
|
|
}
|
|
}
|
|
|
|
_getPositionForDirection(direction, fromWs, toWs, monitor) {
|
|
let xDest = 0, yDest = 0;
|
|
|
|
let condition = w => w.get_monitor() === monitor && w.is_fullscreen();
|
|
|
|
let oldWsIsFullscreen = fromWs.list_windows().some(condition);
|
|
let newWsIsFullscreen = toWs.list_windows().some(condition);
|
|
|
|
let geometry = Main.layoutManager.monitors[monitor];
|
|
|
|
// We have to shift windows up or down by the height of the panel to prevent having a
|
|
// visible gap between the windows while switching workspaces. Since fullscreen windows
|
|
// hide the panel, they don't need to be shifted up or down.
|
|
let shiftHeight = monitor === Main.layoutManager.primaryIndex
|
|
? Main.panel.height : 0;
|
|
|
|
if (direction === Meta.MotionDirection.UP ||
|
|
direction === Meta.MotionDirection.UP_LEFT ||
|
|
direction === Meta.MotionDirection.UP_RIGHT)
|
|
yDest = -geometry.height + (oldWsIsFullscreen ? 0 : shiftHeight);
|
|
else if (direction === Meta.MotionDirection.DOWN ||
|
|
direction === Meta.MotionDirection.DOWN_LEFT ||
|
|
direction === Meta.MotionDirection.DOWN_RIGHT)
|
|
yDest = geometry.height - (newWsIsFullscreen ? 0 : shiftHeight);
|
|
|
|
if (direction === Meta.MotionDirection.LEFT ||
|
|
direction === Meta.MotionDirection.UP_LEFT ||
|
|
direction === Meta.MotionDirection.DOWN_LEFT)
|
|
xDest = -geometry.width;
|
|
else if (direction === Meta.MotionDirection.RIGHT ||
|
|
direction === Meta.MotionDirection.UP_RIGHT ||
|
|
direction === Meta.MotionDirection.DOWN_RIGHT)
|
|
xDest = geometry.width;
|
|
|
|
return [xDest, yDest];
|
|
}
|
|
|
|
directionForProgress(progress) {
|
|
if (global.workspace_manager.layout_rows === -1) {
|
|
return progress > 0
|
|
? Meta.MotionDirection.DOWN
|
|
: Meta.MotionDirection.UP;
|
|
} else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {
|
|
return progress > 0
|
|
? Meta.MotionDirection.LEFT
|
|
: Meta.MotionDirection.RIGHT;
|
|
} else {
|
|
return progress > 0
|
|
? Meta.MotionDirection.RIGHT
|
|
: Meta.MotionDirection.LEFT;
|
|
}
|
|
}
|
|
|
|
progressForDirection(dir) {
|
|
if (global.workspace_manager.layout_rows === -1)
|
|
return dir === Meta.MotionDirection.DOWN ? 1 : -1;
|
|
else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
|
|
return dir === Meta.MotionDirection.LEFT ? 1 : -1;
|
|
else
|
|
return dir === Meta.MotionDirection.RIGHT ? 1 : -1;
|
|
}
|
|
|
|
get progress() {
|
|
return this._progress;
|
|
}
|
|
|
|
set progress(progress) {
|
|
this._progress = progress;
|
|
|
|
let direction = this.directionForProgress(progress);
|
|
|
|
for (let monitorData of this._monitors) {
|
|
let xPos = 0;
|
|
let yPos = 0;
|
|
|
|
if (global.workspace_manager.layout_rows === -1)
|
|
yPos = -Math.round(progress * this._getDistance(monitorData, direction));
|
|
else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
|
|
xPos = Math.round(progress * this._getDistance(monitorData, direction));
|
|
else
|
|
xPos = -Math.round(progress * this._getDistance(monitorData, direction));
|
|
|
|
monitorData.container.set_position(xPos, yPos);
|
|
}
|
|
}
|
|
|
|
_getDistance(monitorData, direction) {
|
|
let info = monitorData.surroundings[direction];
|
|
if (!info)
|
|
return 0;
|
|
|
|
switch (direction) {
|
|
case Meta.MotionDirection.UP:
|
|
return -info.yDest;
|
|
case Meta.MotionDirection.DOWN:
|
|
return info.yDest;
|
|
case Meta.MotionDirection.LEFT:
|
|
return -info.xDest;
|
|
case Meta.MotionDirection.RIGHT:
|
|
return info.xDest;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
getProgressRange(monitor) {
|
|
let monitorData = null;
|
|
for (let data of this._monitors) {
|
|
if (data.index === monitor) {
|
|
monitorData = data;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!monitorData)
|
|
return 0;
|
|
|
|
let baseDistance;
|
|
if (global.workspace_manager.layout_rows !== -1)
|
|
baseDistance = Main.layoutManager.monitors[monitor].width;
|
|
else
|
|
baseDistance = Main.layoutManager.monitors[monitor].height;
|
|
|
|
let direction = this.directionForProgress(-1);
|
|
let distance = this._getDistance(monitorData, direction);
|
|
let lower = -distance / baseDistance;
|
|
|
|
direction = this.directionForProgress(1);
|
|
distance = this._getDistance(monitorData, direction);
|
|
let upper = distance / baseDistance;
|
|
|
|
return [lower, upper];
|
|
}
|
|
});
|
|
|
|
var WorkspaceAnimationController = class {
|
|
constructor() {
|
|
this._blockAnimations = false;
|
|
this._movingWindow = null;
|
|
this._inProgress = false;
|
|
this._gestureActivated = false;
|
|
this._animation = null;
|
|
|
|
Main.overview.connect('showing', () => {
|
|
if (this._gestureActivated)
|
|
this._switchWorkspaceStop();
|
|
|
|
this._swipeTracker.enabled = false;
|
|
});
|
|
Main.overview.connect('hiding', () => {
|
|
this._swipeTracker.enabled = true;
|
|
});
|
|
|
|
let swipeTracker = new SwipeTracker.SwipeTracker(global.stage,
|
|
Shell.ActionMode.NORMAL, { allowDrag: false, allowScroll: false });
|
|
swipeTracker.connect('begin', this._switchWorkspaceBegin.bind(this));
|
|
swipeTracker.connect('update', this._switchWorkspaceUpdate.bind(this));
|
|
swipeTracker.connect('end', this._switchWorkspaceEnd.bind(this));
|
|
this._swipeTracker = swipeTracker;
|
|
}
|
|
|
|
_prepareWorkspaceSwitch(from, to, direction) {
|
|
if (this._animation)
|
|
return;
|
|
|
|
this._animation = new WorkspaceAnimation(this, from, to, direction);
|
|
}
|
|
|
|
_finishWorkspaceSwitch() {
|
|
if (this._animation)
|
|
this._animation.destroy();
|
|
this._animation = null;
|
|
this._inProgress = false;
|
|
this._gestureActivated = false;
|
|
this.movingWindow = null;
|
|
this._monitor = null;
|
|
}
|
|
|
|
animateSwitchWorkspace(from, to, direction, onComplete) {
|
|
this._prepareWorkspaceSwitch(from, to, direction);
|
|
this._inProgress = true;
|
|
|
|
let progress = this._animation.progressForDirection(direction);
|
|
|
|
this._animation.ease_property('progress', progress, {
|
|
duration: WINDOW_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
|
|
onComplete: () => {
|
|
this._finishWorkspaceSwitch();
|
|
onComplete();
|
|
},
|
|
});
|
|
}
|
|
|
|
_switchWorkspaceBegin(tracker, monitor) {
|
|
if (Meta.prefs_get_workspaces_only_on_primary() &&
|
|
monitor !== Main.layoutManager.primaryIndex)
|
|
return;
|
|
|
|
let workspaceManager = global.workspace_manager;
|
|
let horiz = workspaceManager.layout_rows !== -1;
|
|
tracker.orientation = horiz
|
|
? Clutter.Orientation.HORIZONTAL
|
|
: Clutter.Orientation.VERTICAL;
|
|
|
|
let activeWorkspace = workspaceManager.get_active_workspace();
|
|
|
|
let baseDistance;
|
|
if (horiz)
|
|
baseDistance = Main.layoutManager.monitors[monitor].width;
|
|
else
|
|
baseDistance = Main.layoutManager.monitors[monitor].height;
|
|
|
|
let progress;
|
|
if (this._gestureActivated) {
|
|
this._animation.remove_all_transitions();
|
|
progress = this._animation.progress;
|
|
} else {
|
|
this._prepareWorkspaceSwitch(activeWorkspace.index(), -1);
|
|
progress = 0;
|
|
}
|
|
|
|
this._monitor = monitor;
|
|
let [lower, upper] = this._animation.getProgressRange(monitor);
|
|
if (progress < 0)
|
|
progress *= -lower;
|
|
else if (progress > 0)
|
|
progress *= upper;
|
|
|
|
let points = [];
|
|
if (lower !== 0)
|
|
points.push(lower);
|
|
|
|
points.push(0);
|
|
|
|
if (upper !== 0)
|
|
points.push(upper);
|
|
|
|
tracker.confirmSwipe(baseDistance, points, progress, 0);
|
|
}
|
|
|
|
_switchWorkspaceUpdate(tracker, progress) {
|
|
// Translate the progress into [-1;1] range
|
|
let [lower, upper] = this._animation.getProgressRange(this._monitor);
|
|
if (progress < 0)
|
|
progress /= -lower;
|
|
else if (progress > 0)
|
|
progress /= upper;
|
|
|
|
this._animation.progress = progress;
|
|
}
|
|
|
|
_switchWorkspaceEnd(tracker, duration, endProgress) {
|
|
if (!this._animation)
|
|
return;
|
|
|
|
// Translate the progress into [-1;1] range
|
|
endProgress = Math.sign(endProgress);
|
|
|
|
let workspaceManager = global.workspace_manager;
|
|
let activeWorkspace = workspaceManager.get_active_workspace();
|
|
let newWs = activeWorkspace;
|
|
if (endProgress !== 0) {
|
|
let direction = this._animation.directionForProgress(endProgress);
|
|
newWs = activeWorkspace.get_neighbor(direction);
|
|
}
|
|
|
|
this._gestureActivated = true;
|
|
|
|
this._animation.ease_property('progress', endProgress, {
|
|
duration,
|
|
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
|
|
onComplete: () => {
|
|
if (newWs !== activeWorkspace)
|
|
newWs.activate(global.get_current_time());
|
|
this._finishWorkspaceSwitch();
|
|
},
|
|
});
|
|
}
|
|
|
|
_switchWorkspaceStop() {
|
|
this._animation.progress = 0;
|
|
this._finishWorkspaceSwitch();
|
|
}
|
|
|
|
isAnimating() {
|
|
return this._animation !== null;
|
|
}
|
|
|
|
canCancelGesture() {
|
|
return this.isAnimating() && this._gestureActivated;
|
|
}
|
|
|
|
set movingWindow(movingWindow) {
|
|
this._movingWindow = movingWindow;
|
|
}
|
|
|
|
get movingWindow() {
|
|
return this._movingWindow;
|
|
}
|
|
};
|