/** * V-Shell (Vertical Workspaces) * windowManager.js * * @author GdH * @copyright 2022 - 2024 * @license GPL-3.0 * */ 'use strict'; import Clutter from 'gi://Clutter'; import Meta from 'gi://Meta'; import GObject from 'gi://GObject'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as WindowManager from 'resource:///org/gnome/shell/ui/windowManager.js'; import * as WorkspaceAnimation from 'resource:///org/gnome/shell/ui/workspaceAnimation.js'; const MINIMIZE_WINDOW_ANIMATION_TIME = 400; // windowManager.MINIMIZE_WINDOW_ANIMATION_TIME const MINIMIZE_WINDOW_ANIMATION_MODE = Clutter.AnimationMode.EASE_OUT_EXPO; // WindowManager.MINIMIZE_WINDOW_ANIMATION_MODE let Me; let opt; export const WindowManagerModule = class { constructor(me) { Me = me; opt = Me.opt; this._firstActivation = true; this.moduleEnabled = false; this._overrides = null; this._originalMinimizeSigId = 0; this._minimizeSigId = 0; this._originalUnminimizeSigId = 0; this._unminimizeSigId = 0; } cleanGlobals() { Me = null; opt = null; } update(reset) { this.moduleEnabled = opt.get('windowManagerModule'); const conflict = false; reset = reset || !this.moduleEnabled || conflict; // don't even 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(' WindowManagerModule - Keeping untouched'); } _activateModule() { if (!this._overrides) this._overrides = new Me.Util.Overrides(); this._overrides.addOverride('WindowManager', WindowManager.WindowManager.prototype, WindowManagerCommon); if (opt.WS_SWITCHER_CURRENT_MONITOR) this._overrides.addOverride('WorkspaceAnimationController', WorkspaceAnimation.WorkspaceAnimationController.prototype, WorkspaceAnimationController); if (!this._minimizeSigId) { this._originalMinimizeSigId = GObject.signal_handler_find(Main.wm._shellwm, { signalId: 'minimize' }); if (this._originalMinimizeSigId) { Main.wm._shellwm.block_signal_handler(this._originalMinimizeSigId); this._minimizeSigId = Main.wm._shellwm.connect('minimize', WindowManagerCommon._minimizeWindow.bind(Main.wm)); } this._originalUnminimizeSigId = GObject.signal_handler_find(Main.wm._shellwm, { signalId: 'unminimize' }); if (this._originalUnminimizeSigId) { Main.wm._shellwm.block_signal_handler(this._originalUnminimizeSigId); this._unminimizeSigId = Main.wm._shellwm.connect('unminimize', WindowManagerCommon._unminimizeWindow.bind(Main.wm)); } } console.debug(' WindowManagerModule - Activated'); } _disableModule() { if (this._overrides) this._overrides.removeAll(); this._overrides = null; if (this._minimizeSigId) { Main.wm._shellwm.disconnect(this._minimizeSigId); this._minimizeSigId = 0; } if (this._originalMinimizeSigId) { Main.wm._shellwm.unblock_signal_handler(this._originalMinimizeSigId); this._originalMinimizeSigId = 0; } if (this._unminimizeSigId) { Main.wm._shellwm.disconnect(this._unminimizeSigId); this._unminimizeSigId = 0; } if (this._originalUnminimizeSigId) { Main.wm._shellwm.unblock_signal_handler(this._originalUnminimizeSigId); this._originalUnminimizeSigId = 0; } console.debug(' WindowManagerModule - Disabled'); } }; const WindowManagerCommon = { actionMoveWorkspace(workspace) { if (!Main.sessionMode.hasWorkspaces) return; if (opt.WS_SWITCHER_CURRENT_MONITOR) this._switchWorkspaceCurrentMonitor(workspace); else if (!workspace.active) workspace.activate(global.get_current_time()); }, actionMoveWindow(window, workspace) { if (!Main.sessionMode.hasWorkspaces) return; if (!workspace.active) { // This won't have any effect for "always sticky" windows // (like desktop windows or docks) this._workspaceAnimation.movingWindow = window; window.change_workspace(workspace); global.display.clear_mouse_mode(); if (opt.SWITCH_ONLY_CURRENT_MONITOR_WS) { this._switchWorkspaceCurrentMonitor(workspace, window.get_monitor()); window.activate(global.get_current_time()); } else { workspace.activate_with_focus(window, global.get_current_time()); } } }, _switchWorkspaceCurrentMonitor(workspace, monitor) { // const focusedWindow = global.display.get_focus_window(); // const currentMonitor = focusedWindow ? focusedWindow.get_monitor() : global.display.get_current_monitor(); // using focused window to determine the current monitor can lead to inconsistent behavior and switching monitors between switches // depending on which window takes focus on each workspace // mouse pointer is more stable and predictable source const currentMonitor = monitor ? monitor : global.display.get_current_monitor(); const primaryMonitor = currentMonitor === Main.layoutManager.primaryIndex; const nMonitors = Main.layoutManager.monitors.length; const lastIndexCorrection = Meta.prefs_get_dynamic_workspaces() ? 2 : 1; const lastIndex = global.workspaceManager.get_n_workspaces() - lastIndexCorrection; const targetWsIndex = workspace.index(); const activeWs = global.workspaceManager.get_active_workspace(); const activeWsIndex = activeWs.index(); const diff = activeWsIndex - targetWsIndex; let direction = diff > 0 ? Meta.MotionDirection.UP : Meta.MotionDirection.DOWN; if (diff === 0) { // no actual ws to switch, but secondary monitors are always in wraparound mode so we need to get direction direction = activeWsIndex >= lastIndex ? Meta.MotionDirection.DOWN : Meta.MotionDirection.UP; } if (Math.abs(diff) > 1) { // workspace is probably in wraparound mode and just wrapped so so we need to translate direction direction = diff > 0 ? Meta.MotionDirection.DOWN : Meta.MotionDirection.UP; } if (!primaryMonitor) { this._rotateWorkspaces(direction, currentMonitor); return; } // avoid ws rotations if the last empty dynamic workspace is involved, but allow to rotate from the last to the first, if wraparound is enabled if (workspace !== activeWs && !((targetWsIndex > lastIndex && direction === Meta.MotionDirection.DOWN) || (activeWsIndex > lastIndex && targetWsIndex >= lastIndex))) { for (let i = 0; i < nMonitors; i++) { if (i !== currentMonitor) { const oppositeDirection = direction === Meta.MotionDirection.UP ? Meta.MotionDirection.DOWN : Meta.MotionDirection.UP; this._rotateWorkspaces(oppositeDirection, i); } } } workspace.activate(global.get_current_time()); }, _rotateWorkspaces(direction = 0, monitorIndex = -1, step = 1) { step = direction === Meta.MotionDirection.UP ? Number(step) : -step; const monitor = monitorIndex > -1 ? monitorIndex : global.display.get_current_monitor(); // don't move windows to the last empty workspace if dynamic workspaces are enabled const lastIndexCorrection = Meta.prefs_get_dynamic_workspaces() ? 2 : 1; const lastIndex = global.workspaceManager.get_n_workspaces() - lastIndexCorrection; let windows = Me.Util.getWindows(null); for (let win of windows.reverse()) { // avoid moving modal windows as they move with their parents (and vice versa) immediately, before we move the parent window. if (win.get_monitor() === monitor && !win.is_always_on_all_workspaces() && !win.is_attached_dialog() && !win.get_transient_for()) { let wWs = win.get_workspace().index(); wWs += step; if (wWs < 0) wWs = lastIndex; if (wWs > lastIndex) wWs = 0; const ws = global.workspaceManager.get_workspace_by_index(wWs); win.change_workspace(ws); } } }, // fix for mainstream bug - fullscreen windows should minimize using opacity transition // but its being applied directly on window actor and that doesn't work // anyway, animation is better, even if the Activities button is not visible... // and also add support for bottom position of the panel _minimizeWindow(shellwm, actor) { const types = [ Meta.WindowType.NORMAL, Meta.WindowType.MODAL_DIALOG, Meta.WindowType.DIALOG, ]; if (!this._shouldAnimateActor(actor, types)) { shellwm.completed_minimize(actor); return; } actor.set_scale(1.0, 1.0); this._minimizing.add(actor); /* if (actor.meta_window.is_monitor_sized()) { actor.get_first_child().ease({ opacity: 0, duration: MINIMIZE_WINDOW_ANIMATION_TIME, mode: MINIMIZE_WINDOW_ANIMATION_MODE, onStopped: () => this._minimizeWindowDone(shellwm, actor), }); } else { */ let xDest, yDest, xScale, yScale; let [success, geom] = actor.meta_window.get_icon_geometry(); if (success) { xDest = geom.x; yDest = geom.y; xScale = geom.width / actor.width; yScale = geom.height / actor.height; } else { let monitor = Main.layoutManager.monitors[actor.meta_window.get_monitor()]; if (!monitor) { this._minimizeWindowDone(); return; } xDest = monitor.x; yDest = opt.PANEL_POSITION_TOP ? monitor.y : monitor.y + monitor.height; if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) xDest += monitor.width; xScale = 0; yScale = 0; } actor.ease({ scale_x: xScale, scale_y: yScale, x: xDest, y: yDest, duration: MINIMIZE_WINDOW_ANIMATION_TIME, mode: MINIMIZE_WINDOW_ANIMATION_MODE, onStopped: () => this._minimizeWindowDone(shellwm, actor), }); // } }, _minimizeWindowDone(shellwm, actor) { if (this._minimizing.delete(actor)) { actor.remove_all_transitions(); actor.set_scale(1.0, 1.0); actor.get_first_child().set_opacity(255); actor.set_pivot_point(0, 0); shellwm.completed_minimize(actor); } }, _unminimizeWindow(shellwm, actor) { const types = [ Meta.WindowType.NORMAL, Meta.WindowType.MODAL_DIALOG, Meta.WindowType.DIALOG, ]; if (!this._shouldAnimateActor(actor, types)) { shellwm.completed_unminimize(actor); return; } this._unminimizing.add(actor); /* if (false/* actor.meta_window.is_monitor_sized()) { actor.opacity = 0; actor.set_scale(1.0, 1.0); actor.ease({ opacity: 255, duration: MINIMIZE_WINDOW_ANIMATION_TIME, mode: MINIMIZE_WINDOW_ANIMATION_MODE, onStopped: () => this._unminimizeWindowDone(shellwm, actor), }); } else { */ let [success, geom] = actor.meta_window.get_icon_geometry(); if (success) { actor.set_position(geom.x, geom.y); actor.set_scale(geom.width / actor.width, geom.height / actor.height); } else { let monitor = Main.layoutManager.monitors[actor.meta_window.get_monitor()]; if (!monitor) { actor.show(); this._unminimizeWindowDone(); return; } actor.set_position(monitor.x, opt.PANEL_POSITION_TOP ? monitor.y : monitor.y + monitor.height); if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) actor.x += monitor.width; actor.set_scale(0, 0); } let rect = actor.meta_window.get_buffer_rect(); let [xDest, yDest] = [rect.x, rect.y]; actor.show(); actor.ease({ scale_x: 1, scale_y: 1, x: xDest, y: yDest, duration: MINIMIZE_WINDOW_ANIMATION_TIME, mode: MINIMIZE_WINDOW_ANIMATION_MODE, onStopped: () => this._unminimizeWindowDone(shellwm, actor), }); // } }, }; const WorkspaceAnimationController = { _prepareWorkspaceSwitch(workspaceIndices) { if (this._switchData) return; const workspaceManager = global.workspace_manager; const nWorkspaces = workspaceManager.get_n_workspaces(); const switchData = {}; this._switchData = switchData; switchData.monitors = []; switchData.gestureActivated = false; switchData.inProgress = false; if (!workspaceIndices) workspaceIndices = [...Array(nWorkspaces).keys()]; let monitors = opt.WS_SWITCHER_CURRENT_MONITOR ? [Main.layoutManager.currentMonitor] : Main.layoutManager.monitors; monitors = Meta.prefs_get_workspaces_only_on_primary() ? [Main.layoutManager.primaryMonitor] : monitors; for (const monitor of monitors) { if (Meta.prefs_get_workspaces_only_on_primary() && monitor.index !== Main.layoutManager.primaryIndex) continue; const group = new WorkspaceAnimation.MonitorGroup(monitor, workspaceIndices, this.movingWindow); Main.uiGroup.insert_child_above(group, global.window_group); switchData.monitors.push(group); } Meta.disable_unredirect_for_display(global.display); }, };