// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- /* exported WorkspaceAnimationController */ const { Clutter, GObject, Meta, Shell } = imports.gi; const Background = imports.ui.background; const Layout = imports.ui.layout; const Main = imports.ui.main; const SwipeTracker = imports.ui.swipeTracker; const WINDOW_ANIMATION_TIME = 250; const WorkspaceGroup = GObject.registerClass( class WorkspaceGroup extends Clutter.Actor { _init(workspace, movingWindow) { super._init(); this._workspace = workspace; this._movingWindow = movingWindow; this._windowRecords = []; this._createWindows(); this.connect('destroy', this._onDestroy.bind(this)); this._restackedId = global.display.connect('restacked', this._syncStacking.bind(this)); } get workspace() { return this._workspace; } _shouldShowWindow(window) { if (!window.showing_on_its_workspace()) return false; const isSticky = window.is_on_all_workspaces() || window === this._movingWindow; return !isSticky && window.located_on_workspace(this._workspace); } _syncStacking() { const windowActors = global.get_window_actors().filter(w => this._shouldShowWindow(w.meta_window)); let lastRecord; for (const windowActor of windowActors) { const record = this._windowRecords.find(r => r.windowActor === windowActor); this.set_child_above_sibling(record.clone, lastRecord ? lastRecord.clone : null); lastRecord = record; } } _createWindows() { const windowActors = global.get_window_actors().filter(w => this._shouldShowWindow(w.meta_window)); for (const windowActor of windowActors) { const clone = new Clutter.Clone({ source: windowActor, x: windowActor.x, y: windowActor.y, }); this.add_child(clone); const record = { windowActor, clone }; record.windowDestroyId = windowActor.connect('destroy', () => { clone.destroy(); this._windowRecords.splice(this._windowRecords.indexOf(record), 1); }); this._windowRecords.push(record); } } _removeWindows() { for (const record of this._windowRecords) { record.windowActor.disconnect(record.windowDestroyId); record.clone.destroy(); } this._windowRecords = []; } _onDestroy() { global.display.disconnect(this._restackedId); this._removeWindows(); } }); const MonitorGroup = GObject.registerClass( class MonitorGroup extends Clutter.Actor { _init(monitor) { super._init(); const constraint = new Layout.MonitorConstraint({ index: monitor.index }); this.add_constraint(constraint); const background = new Meta.BackgroundGroup(); this.add_child(background); this._bgManager = new Background.BackgroundManager({ container: background, monitorIndex: monitor.index, controlPosition: false, }); this.connect('destroy', this._onDestroy.bind(this)); } _onDestroy() { this._bgManager.destroy(); } }); var WorkspaceAnimationController = class { constructor() { this._movingWindow = null; this._switchData = null; Main.overview.connect('showing', () => { if (this._switchData) { if (this._switchData.gestureActivated) this._finishWorkspaceSwitch(this._switchData); 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(workspaceIndices) { if (this._switchData) return; const workspaceManager = global.workspace_manager; const vertical = workspaceManager.layout_rows === -1; const nWorkspaces = workspaceManager.get_n_workspaces(); const activeWorkspaceIndex = workspaceManager.get_active_workspace_index(); const switchData = {}; this._switchData = switchData; switchData.movingWindowBin = new Clutter.Actor(); switchData.movingWindow = null; switchData.workspaceGroups = []; switchData.gestureActivated = false; switchData.inProgress = false; switchData.container = new Clutter.Actor(); switchData.backgroundGroup = new Clutter.Actor(); for (const monitor of Main.layoutManager.monitors) switchData.backgroundGroup.add_child(new MonitorGroup(monitor)); Main.uiGroup.insert_child_above(switchData.backgroundGroup, global.window_group); Main.uiGroup.insert_child_above(switchData.container, switchData.backgroundGroup); Main.uiGroup.insert_child_above(switchData.movingWindowBin, switchData.container); let x = 0; let y = 0; if (!workspaceIndices) workspaceIndices = [...Array(nWorkspaces).keys()]; for (const i of workspaceIndices) { const ws = workspaceManager.get_workspace_by_index(i); const fullscreen = ws.list_windows().some(w => w.is_fullscreen()); if (y > 0 && vertical && !fullscreen) { // 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. y -= Main.panel.height; } const group = new WorkspaceGroup(ws, this.movingWindow); switchData.workspaceGroups.push(group); switchData.container.add_child(group); switchData.container.set_child_above_sibling(group, null); group.set_position(x, y); if (vertical) y += global.screen_height; else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) x -= global.screen_width; else x += global.screen_width; } const activeGroup = this._findWorkspaceGroupByIndex(activeWorkspaceIndex); if (vertical) switchData.container.y = -activeGroup.y; else switchData.container.x = -activeGroup.x; if (this.movingWindow) { const windowActor = this.movingWindow.get_compositor_private(); switchData.movingWindow = { windowActor, parent: windowActor.get_parent(), }; switchData.movingWindow.parent.remove_child(windowActor); switchData.movingWindowBin.add_child(windowActor); switchData.movingWindow.windowDestroyId = windowActor.connect('destroy', () => { switchData.movingWindow = null; }); } } _finishWorkspaceSwitch(switchData) { this._switchData = null; if (switchData.movingWindow) { const record = switchData.movingWindow; record.windowActor.disconnect(record.windowDestroyId); switchData.movingWindowBin.remove_child(record.windowActor); record.parent.add_child(record.windowActor); switchData.movingWindow = null; } switchData.backgroundGroup.destroy(); switchData.container.destroy(); switchData.movingWindowBin.destroy(); this.movingWindow = null; } animateSwitch(from, to, direction, onComplete) { this._swipeTracker.enabled = false; let workspaceIndices = []; switch (direction) { case Meta.MotionDirection.UP: case Meta.MotionDirection.LEFT: case Meta.MotionDirection.UP_LEFT: case Meta.MotionDirection.UP_RIGHT: workspaceIndices = [to, from]; break; case Meta.MotionDirection.DOWN: case Meta.MotionDirection.RIGHT: case Meta.MotionDirection.DOWN_LEFT: case Meta.MotionDirection.DOWN_RIGHT: workspaceIndices = [from, to]; break; } if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL && direction !== Meta.MotionDirection.UP && direction !== Meta.MotionDirection.DOWN) workspaceIndices.reverse(); this._prepareWorkspaceSwitch(workspaceIndices); this._switchData.inProgress = true; const fromGroup = this._findWorkspaceGroupByIndex(from); const toGroup = this._findWorkspaceGroupByIndex(to); this._switchData.container.x = -fromGroup.x; this._switchData.container.y = -fromGroup.y; this._switchData.container.ease({ x: -toGroup.x, y: -toGroup.y, duration: WINDOW_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_CUBIC, onComplete: () => { this._finishWorkspaceSwitch(this._switchData); onComplete(); this._swipeTracker.enabled = true; }, }); } _getProgressForWorkspace(workspaceGroup) { if (global.workspace_manager.layout_rows === -1) return workspaceGroup.y / global.screen_height; else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) return -workspaceGroup.x / global.screen_width; else return workspaceGroup.x / global.screen_width; } _findWorkspaceGroupByIndex(index) { return this._switchData.workspaceGroups.find(g => g.workspace.index() === index); } _findClosestWorkspaceGroup(progress) { const distances = this._switchData.workspaceGroups.map(g => { const workspaceProgress = this._getProgressForWorkspace(g); return Math.abs(workspaceProgress - progress); }); const index = distances.indexOf(Math.min(...distances)); return this._switchData.workspaceGroups[index]; } _switchWorkspaceBegin(tracker, monitor) { if (Meta.prefs_get_workspaces_only_on_primary() && monitor !== Main.layoutManager.primaryIndex) return; const workspaceManager = global.workspace_manager; const horiz = workspaceManager.layout_rows !== -1; tracker.orientation = horiz ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; const baseDistance = horiz ? global.screen_width : global.screen_height; let progress; let cancelProgress; if (this._switchData && this._switchData.gestureActivated) { this._switchData.container.remove_all_transitions(); if (!horiz) progress = -this._switchData.container.y / baseDistance; else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) progress = this._switchData.container.x / baseDistance; else progress = -this._switchData.container.x / baseDistance; const wsGroup = this._findClosestWorkspaceGroup(progress); cancelProgress = this._getProgressForWorkspaceGroup(wsGroup); } else { this._prepareWorkspaceSwitch(); const activeIndex = workspaceManager.get_active_workspace_index(); const wsGroup = this._findWorkspaceGroupByIndex(activeIndex); progress = cancelProgress = this._getProgressForWorkspace(wsGroup); } const points = this._switchData.workspaceGroups.map(this._getProgressForWorkspace); tracker.confirmSwipe(baseDistance, points, progress, cancelProgress); } _switchWorkspaceUpdate(tracker, progress) { if (!this._switchData) return; let xPos = 0; let yPos = 0; if (global.workspace_manager.layout_rows === -1) yPos = -Math.round(progress * global.screen_height); else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) xPos = Math.round(progress * global.screen_width); else xPos = -Math.round(progress * global.screen_width); this._switchData.container.set_position(xPos, yPos); } _switchWorkspaceEnd(tracker, duration, endProgress) { if (!this._switchData) return; const newGroup = this._findClosestWorkspaceGroup(endProgress); const switchData = this._switchData; switchData.gestureActivated = true; this._switchData.container.ease({ x: -newGroup.x, y: -newGroup.y, duration, mode: Clutter.AnimationMode.EASE_OUT_CUBIC, onComplete: () => { if (!newGroup.workspace.active) newGroup.workspace.activate(global.get_current_time()); this._finishWorkspaceSwitch(switchData); }, }); } get gestureActive() { return this._switchData !== null && this._switchData.gestureActivated; } cancelSwitchAnimation() { if (!this._switchData) return; if (this._switchData.gestureActivated) return; this._finishWorkspaceSwitch(this._switchData); } set movingWindow(movingWindow) { this._movingWindow = movingWindow; } get movingWindow() { return this._movingWindow; } };