/* * This file is part of the Forge extension for GNOME * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ // Gnome imports import GLib from "gi://GLib"; import Clutter from "gi://Clutter"; import GObject from "gi://GObject"; import Meta from "gi://Meta"; import St from "gi://St"; // Gnome Shell imports import { gettext as _ } from "resource:///org/gnome/shell/extensions/extension.js"; import * as Main from "resource:///org/gnome/shell/ui/main.js"; import { PACKAGE_VERSION } from "resource:///org/gnome/shell/misc/config.js"; // Shared state import { Logger } from "../shared/logger.js"; // App imports import * as Utils from "./utils.js"; import { Keybindings } from "./keybindings.js"; import { Tree, Queue, Node, POSITION, LAYOUT_TYPES, ORIENTATION_TYPES, NODE_TYPES, } from "./tree.js"; import { production } from "../shared/settings.js"; /** @typedef {import('../../extension.js').default} ForgeExtension */ export const WINDOW_MODES = Utils.createEnum(["FLOAT", "TILE", "GRAB_TILE", "DEFAULT"]); // Simplify the grab modes export const GRAB_TYPES = Utils.createEnum(["RESIZING", "MOVING", "UNKNOWN"]); export class WindowManager extends GObject.Object { static { GObject.registerClass(this); } /** @type {ForgeExtension} */ ext; /** @param {ForgeExtension} ext */ constructor(ext) { super(); this.ext = ext; this.prefsTitle = `Forge ${_("Settings")} - ${ !production ? "DEV" : `${PACKAGE_VERSION}-${ext.metadata.version}` }`; this.windowProps = this.ext.configMgr.windowProps; this.windowProps.overrides = this.windowProps.overrides.filter((override) => !override.wmId); this._kbd = this.ext.keybindings; this._tree = new Tree(this); this.eventQueue = new Queue(); this.theme = this.ext.theme; this.lastFocusedWindow = null; Logger.info("forge initialized"); } addFloatOverride(metaWindow, withWmId) { let overrides = this.windowProps.overrides; let wmClass = metaWindow.get_wm_class(); let wmId = metaWindow.get_id(); for (let override in overrides) { // if the window is already floating if (override.wmClass === wmClass && override.mode === "float" && !override.wmTitle) return; } overrides.push({ wmClass: wmClass, wmId: withWmId ? wmId : undefined, mode: "float", }); this.windowProps.overrides = overrides; this.ext.configMgr.windowProps = this.windowProps; } removeFloatOverride(metaWindow, withWmId) { let overrides = this.windowProps.overrides; let wmClass = metaWindow.get_wm_class(); let wmId = metaWindow.get_id(); overrides = overrides.filter( (override) => !( override.wmClass === wmClass && // rules with a Title are written by the user and peristent !override.wmTitle && (!withWmId || override.wmId === wmId) ) ); this.windowProps.overrides = overrides; this.ext.configMgr.windowProps = this.windowProps; } toggleFloatingMode(action, metaWindow) { let nodeWindow = this.findNodeWindow(metaWindow); if (!nodeWindow || !(action || action.mode)) return; if (nodeWindow.nodeType !== NODE_TYPES.WINDOW) return; let withWmId = action.name === "FloatToggle"; let floatingExempt = this.isFloatingExempt(metaWindow); if (floatingExempt) { this.removeFloatOverride(metaWindow, withWmId); if (!this.isActiveWindowWorkspaceTiled(metaWindow)) { nodeWindow.mode = WINDOW_MODES.FLOAT; } else { nodeWindow.mode = WINDOW_MODES.TILE; } } else { this.addFloatOverride(metaWindow, withWmId); nodeWindow.mode = WINDOW_MODES.FLOAT; } } queueEvent(eventObj, interval = 220) { this.eventQueue.enqueue(eventObj); if (!this._queueSourceId) { this._queueSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => { const currEventObj = this.eventQueue.dequeue(); if (currEventObj) { currEventObj.callback(); } const result = this.eventQueue.length !== 0; if (!result) { this._queueSourceId = 0; } return result; }); } } /** * This is the central place to bind all the non-window signals. */ _bindSignals() { if (this._signalsBound) return; const display = global.display; const shellWm = global.window_manager; this._displaySignals = [ display.connect("window-created", this.trackWindow.bind(this)), display.connect("grab-op-begin", this._handleGrabOpBegin.bind(this)), display.connect("window-entered-monitor", (_, monitor, metaWindow) => { this.updateMetaWorkspaceMonitor("window-entered-monitor", monitor, metaWindow); this.trackCurrentMonWs(); }), display.connect("grab-op-end", this._handleGrabOpEnd.bind(this)), display.connect("showing-desktop-changed", () => { this.hideWindowBorders(); this.updateDecorationLayout(); }), display.connect("in-fullscreen-changed", () => { this.renderTree("full-screen-changed"); }), display.connect("workareas-changed", (_display) => { if (global.display.get_n_monitors() == 0) { Logger.debug(`workareas-changed: no monitors, ignoring signal`); return; } if (this.tree.getNodeByType("WINDOW").length > 0) { let workspaceReload = this.workspaceAdded || this.workspaceRemoved; if (workspaceReload) { this.trackCurrentWindows(); this.workspaceRemoved = false; this.workspaceAdded = false; } else { this.renderTree("workareas-changed"); } } }), ]; this._windowManagerSignals = [ shellWm.connect("minimize", () => { this.hideWindowBorders(); let focusNodeWindow = this.tree.findNode(this.focusMetaWindow); if (focusNodeWindow) { if (this.tree.getTiledChildren(focusNodeWindow.parentNode.childNodes).length === 0) { this.tree.resetSiblingPercent(focusNodeWindow.parentNode.parentNode); } this.tree.resetSiblingPercent(focusNodeWindow.parentNode); } let prevFrozen = this._freezeRender; if (prevFrozen) this.unfreezeRender(); this.renderTree("minimize"); if (prevFrozen) this.freezeRender(); }), shellWm.connect("unminimize", () => { let focusNodeWindow = this.tree.findNode(this.focusMetaWindow); if (focusNodeWindow) { this.tree.resetSiblingPercent(focusNodeWindow.parentNode); } let prevFrozen = this._freezeRender; if (prevFrozen) this.unfreezeRender(); this.renderTree("unminimize"); if (prevFrozen) this.freezeRender(); }), shellWm.connect("show-tile-preview", (_, _metaWindow, _rect, _num) => { // Empty }), ]; const globalWsm = global.workspace_manager; this._workspaceManagerSignals = [ globalWsm.connect("showing-desktop-changed", () => { this.hideWindowBorders(); this.updateDecorationLayout(); }), globalWsm.connect("workspace-added", (_, wsIndex) => { this.tree.addWorkspace(wsIndex); this.trackCurrentMonWs(); this.workspaceAdded = true; this.renderTree("workspace-added"); }), globalWsm.connect("workspace-removed", (_, wsIndex) => { this.tree.removeWorkspace(wsIndex); this.trackCurrentMonWs(); this.workspaceRemoved = true; this.updateDecorationLayout(); this.renderTree("workspace-removed"); }), globalWsm.connect("active-workspace-changed", () => { this.hideWindowBorders(); this.trackCurrentMonWs(); this.updateDecorationLayout(); this.renderTree("active-workspace-changed"); }), ]; let numberOfWorkspaces = globalWsm.get_n_workspaces(); for (let i = 0; i < numberOfWorkspaces; i++) { let workspace = globalWsm.get_workspace_by_index(i); this.bindWorkspaceSignals(workspace); } let settings = this.ext.settings; settings.connect("changed", (_, settingName) => { switch (settingName) { case "focus-border-toggle": this.renderTree(settingName); break; case "tiling-mode-enabled": this.renderTree(settingName); break; case "window-gap-size-increment": case "window-gap-size": case "window-gap-hidden-on-single": case "workspace-skip-tile": this.renderTree(settingName, true); break; case "stacked-tiling-mode-enabled": if (!settings.get_boolean(settingName)) { let stackedNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.STACKED); stackedNodes.forEach((node) => { node.prevLayout = node.layout; node.layout = this.determineSplitLayout(); }); } else { let hSplitNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.HSPLIT); let vSplitNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.VSPLIT); Array.prototype.push.apply(hSplitNodes, vSplitNodes); hSplitNodes.forEach((node) => { if (node.prevLayout && node.prevLayout === LAYOUT_TYPES.STACKED) { node.layout = LAYOUT_TYPES.STACKED; } }); } this.renderTree(settingName); break; case "tabbed-tiling-mode-enabled": if (!settings.get_boolean(settingName)) { let tabbedNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.TABBED); tabbedNodes.forEach((node) => { node.prevLayout = node.layout; node.layout = this.determineSplitLayout(); }); } else { let hSplitNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.HSPLIT); let vSplitNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.VSPLIT); Array.prototype.push.apply(hSplitNodes, vSplitNodes); hSplitNodes.forEach((node) => { if (node.prevLayout && node.prevLayout === LAYOUT_TYPES.TABBED) { node.layout = LAYOUT_TYPES.TABBED; } }); } this.renderTree(settingName); break; case "css-updated": this.theme.reloadStylesheet(); break; case "float-always-on-top-enabled": if (!settings.get_boolean(settingName)) { this.cleanupAlwaysFloat(); } else { this.restoreAlwaysFloat(); } break; default: break; } }); this._overviewSignals = [ Main.overview.connect("hiding", () => { this.fromOverview = true; const eventObj = { name: "focus-after-overview", callback: () => { const focusNodeWindow = this.tree.findNode(this.focusMetaWindow); this.updateStackedFocus(focusNodeWindow); this.updateTabbedFocus(focusNodeWindow); this.movePointerWith(focusNodeWindow); }, }; this.queueEvent(eventObj); }), Main.overview.connect("showing", () => { this.toOverview = true; }), ]; this._signalsBound = true; } cleanupAlwaysFloat() { // remove the setting for each node window this.allNodeWindows.forEach((w) => { if (w.mode === WINDOW_MODES.FLOAT) { w.nodeValue.is_above() && w.nodeValue.unmake_above(); } }); } restoreAlwaysFloat() { this.allNodeWindows.forEach((w) => { if (w.mode === WINDOW_MODES.FLOAT) { !w.nodeValue.is_above() && w.nodeValue.make_above(); } }); } trackCurrentMonWs() { let metaWindow = this.focusMetaWindow; if (!metaWindow) return; const currentMonitor = global.display.get_current_monitor(); const currentWorkspace = global.display.get_workspace_manager().get_active_workspace_index(); let currentMonWs = `mo${currentMonitor}ws${currentWorkspace}`; let activeMetaMonWs = `mo${metaWindow.get_monitor()}ws${metaWindow.get_workspace().index()}`; let currentWsNode = this.tree.findNode(`ws${currentWorkspace}`); if (!currentWsNode) { return; } // Search for all the valid windows on the workspace const monWindows = currentWsNode.getNodeByType(NODE_TYPES.WORKSPACE).flatMap((ws) => { return ws .getNodeByType(NODE_TYPES.WINDOW) .filter( (w) => !w.nodeValue.minimized && w.isTile() && w.nodeValue !== metaWindow && // The searched window should be on the same monitor workspace // This ensures that Forge already updated the workspace node tree: currentMonWs === activeMetaMonWs ) .map((w) => w.nodeValue); }); this.sortedWindows = global.display.sort_windows_by_stacking(monWindows).reverse(); } // TODO move this to workspace.js bindWorkspaceSignals(metaWorkspace) { if (metaWorkspace) { if (!metaWorkspace.workspaceSignals) { let workspaceSignals = [ metaWorkspace.connect("window-added", (_, metaWindow) => { if (!this._wsWindowAddSrcId) { this._wsWindowAddSrcId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => { this.updateMetaWorkspaceMonitor( "window-added", metaWindow.get_monitor(), metaWindow ); this._wsWindowAddSrcId = 0; return false; }); } }), ]; metaWorkspace.workspaceSignals = workspaceSignals; } } } // TODO move this in command.js command(action) { let focusWindow = this.focusMetaWindow; // Do not check if the node window is null, some of the commands do not need the focus window let focusNodeWindow = this.findNodeWindow(focusWindow); let currentLayout; switch (action.name) { case "FloatNonPersistentToggle": case "FloatToggle": case "FloatClassToggle": this.toggleFloatingMode(action, focusWindow); const rectRequest = { x: action.x, y: action.y, width: action.width, height: action.height, }; let moveRect = { x: Utils.resolveX(rectRequest, focusWindow), y: Utils.resolveY(rectRequest, focusWindow), width: Utils.resolveWidth(rectRequest, focusWindow), height: Utils.resolveHeight(rectRequest, focusWindow), }; this.move(focusWindow, moveRect); let existParent = focusNodeWindow.parentNode; if (this.tree.getTiledChildren(existParent.childNodes).length <= 1) { existParent.percent = 0.0; this.tree.resetSiblingPercent(existParent.parentNode); } this.tree.resetSiblingPercent(existParent); this.renderTree("float-toggle", true); break; case "Move": this.unfreezeRender(); let moveDirection = Utils.resolveDirection(action.direction); let prev = focusNodeWindow; let moved = this.tree.move(focusNodeWindow, moveDirection); if (!focusNodeWindow) { focusNodeWindow = this.findNodeWindow(this.focusMetaWindow); } this.queueEvent({ name: "move", callback: () => { if (this.eventQueue.length <= 0) { this.unfreezeRender(); if (focusNodeWindow.parentNode.layout === LAYOUT_TYPES.STACKED) { focusNodeWindow.parentNode.appendChild(focusNodeWindow); focusNodeWindow.nodeValue.raise(); focusNodeWindow.nodeValue.activate(global.display.get_current_time()); this.renderTree("move-stacked-queue"); } if (focusNodeWindow.parentNode.layout === LAYOUT_TYPES.TABBED) { focusNodeWindow.nodeValue.raise(); focusNodeWindow.nodeValue.activate(global.display.get_current_time()); if (prev) prev.parentNode.lastTabFocus = prev.nodeValue; this.renderTree("move-tabbed-queue"); } this.movePointerWith(focusNodeWindow); } }, }); if (moved) { if (prev) prev.parentNode.lastTabFocus = prev.nodeValue; this.renderTree("move-window"); } break; case "Focus": let focusDirection = Utils.resolveDirection(action.direction); focusNodeWindow = this.tree.focus(focusNodeWindow, focusDirection); if (!focusNodeWindow) { focusNodeWindow = this.findNodeWindow(this.focusMetaWindow); } break; case "Swap": if (!focusNodeWindow) return; this.unfreezeRender(); let swapDirection = Utils.resolveDirection(action.direction); this.tree.swap(focusNodeWindow, swapDirection); focusNodeWindow.nodeValue.raise(); this.updateTabbedFocus(focusNodeWindow); this.updateStackedFocus(focusNodeWindow); this.movePointerWith(focusNodeWindow); this.renderTree("swap", true); break; case "Split": if (!focusNodeWindow) return; currentLayout = focusNodeWindow.parentNode.layout; if (currentLayout === LAYOUT_TYPES.STACKED || currentLayout === LAYOUT_TYPES.TABBED) { return; } let orientation = action.orientation ? action.orientation.toUpperCase() : ORIENTATION_TYPES.NONE; this.tree.split(focusNodeWindow, orientation); this.renderTree("split"); break; case "LayoutToggle": if (!focusNodeWindow) return; currentLayout = focusNodeWindow.parentNode.layout; if (currentLayout === LAYOUT_TYPES.HSPLIT) { focusNodeWindow.parentNode.layout = LAYOUT_TYPES.VSPLIT; } else if (currentLayout === LAYOUT_TYPES.VSPLIT) { focusNodeWindow.parentNode.layout = LAYOUT_TYPES.HSPLIT; } this.tree.attachNode = focusNodeWindow.parentNode; this.renderTree("layout-split-toggle"); break; case "FocusBorderToggle": let focusBorderEnabled = this.ext.settings.get_boolean("focus-border-toggle"); this.ext.settings.set_boolean("focus-border-toggle", !focusBorderEnabled); break; case "TilingModeToggle": // FIXME, not sure if this toggle is still needed from a use case // perspective, since Extension.disable also should do the same thing. let tilingModeEnabled = this.ext.settings.get_boolean("tiling-mode-enabled"); this.ext.settings.set_boolean("tiling-mode-enabled", !tilingModeEnabled); if (tilingModeEnabled) { this.floatAllWindows(); } else { this.unfloatAllWindows(); } this.renderTree(`tiling-mode-toggle ${!tilingModeEnabled}`); break; case "GapSize": let gapIncrement = this.ext.settings.get_uint("window-gap-size-increment"); let amount = action.amount; gapIncrement = gapIncrement + amount; if (gapIncrement < 0) gapIncrement = 0; if (gapIncrement > 8) gapIncrement = 8; this.ext.settings.set_uint("window-gap-size-increment", gapIncrement); break; case "WorkspaceActiveTileToggle": let activeWorkspace = global.workspace_manager.get_active_workspace_index(); let skippedWorkspaces = this.ext.settings.get_string("workspace-skip-tile"); let workspaceSkipped = false; let skippedArr = []; if (skippedWorkspaces.length === 0) { skippedArr.push(`${activeWorkspace}`); this.floatWorkspace(activeWorkspace); } else { skippedArr = skippedWorkspaces.split(","); for (let i = 0; i < skippedArr.length; i++) { if (`${skippedArr[i]}` === `${activeWorkspace}`) { workspaceSkipped = true; break; } } if (workspaceSkipped) { // tile this workspace let indexWs = skippedArr.indexOf(`${activeWorkspace}`); skippedArr.splice(indexWs, 1); this.unfloatWorkspace(activeWorkspace); } else { // skip tiling workspace skippedArr.push(`${activeWorkspace}`); this.floatWorkspace(activeWorkspace); } } this.ext.settings.set_string("workspace-skip-tile", skippedArr.toString()); this.renderTree("workspace-toggle"); break; case "LayoutStackedToggle": if (!focusNodeWindow) return; if (!this.ext.settings.get_boolean("stacked-tiling-mode-enabled")) return; if (focusNodeWindow.parentNode.isMonitor()) { this.tree.split(focusNodeWindow, ORIENTATION_TYPES.HORIZONTAL, true); } currentLayout = focusNodeWindow.parentNode.layout; if (currentLayout === LAYOUT_TYPES.STACKED) { focusNodeWindow.parentNode.layout = this.determineSplitLayout(); this.tree.resetSiblingPercent(focusNodeWindow.parentNode); } else { if (currentLayout === LAYOUT_TYPES.TABBED) { focusNodeWindow.parentNode.lastTabFocus = null; } focusNodeWindow.parentNode.layout = LAYOUT_TYPES.STACKED; let lastChild = focusNodeWindow.parentNode.lastChild; if (lastChild.nodeType === NODE_TYPES.WINDOW) { lastChild.nodeValue.activate(global.display.get_current_time()); } } this.unfreezeRender(); this.tree.attachNode = focusNodeWindow.parentNode; this.renderTree("layout-stacked-toggle"); break; case "LayoutTabbedToggle": if (!focusNodeWindow) return; if (!this.ext.settings.get_boolean("tabbed-tiling-mode-enabled")) return; if (focusNodeWindow.parentNode.isMonitor()) { this.tree.split(focusNodeWindow, ORIENTATION_TYPES.HORIZONTAL, true); } currentLayout = focusNodeWindow.parentNode.layout; if (currentLayout === LAYOUT_TYPES.TABBED) { focusNodeWindow.parentNode.layout = this.determineSplitLayout(); this.tree.resetSiblingPercent(focusNodeWindow.parentNode); focusNodeWindow.parentNode.lastTabFocus = null; } else { focusNodeWindow.parentNode.layout = LAYOUT_TYPES.TABBED; focusNodeWindow.parentNode.lastTabFocus = focusNodeWindow.nodeValue; } this.unfreezeRender(); this.tree.attachNode = focusNodeWindow.parentNode; this.renderTree("layout-tabbed-toggle"); break; case "CancelOperation": if (focusNodeWindow.mode === WINDOW_MODES.GRAB_TILE) { this.cancelGrab = true; } break; case "PrefsOpen": let existWindow = Utils.findWindowWith(this.prefsTitle); if (existWindow && existWindow.get_workspace()) { existWindow .get_workspace() .activate_with_focus(existWindow, global.display.get_current_time()); this.moveCenter(existWindow); } else { this.ext.openPreferences(); } break; case "WindowSwapLastActive": if (focusNodeWindow) { let lastActiveWindow = global.display.get_tab_next( Meta.TabList.NORMAL, global.display.get_workspace_manager().get_active_workspace(), focusNodeWindow.nodeValue, false ); let lastActiveNodeWindow = this.tree.findNode(lastActiveWindow); this.tree.swapPairs(lastActiveNodeWindow, focusNodeWindow); this.movePointerWith(focusNodeWindow); this.renderTree("swap-last-active"); } break; case "SnapLayoutMove": if (focusNodeWindow) { let workareaRect = focusNodeWindow.nodeValue.get_work_area_current_monitor(); let layoutAmount = action.amount; let layoutDirection = action.direction.toUpperCase(); let layout = {}; let processGap = false; switch (layoutDirection) { case "LEFT": layout.width = layoutAmount * workareaRect.width; layout.height = workareaRect.height; layout.x = workareaRect.x; layout.y = workareaRect.y; processGap = true; break; case "RIGHT": layout.width = layoutAmount * workareaRect.width; layout.height = workareaRect.height; layout.x = workareaRect.x + (workareaRect.width - layout.width); layout.y = workareaRect.y; processGap = true; break; case "CENTER": let metaRect = this.focusMetaWindow.get_frame_rect(); layout.x = "center"; layout.y = "center"; layout = { x: Utils.resolveX(layout, this.focusMetaWindow), y: Utils.resolveY(layout, this.focusMetaWindow), width: metaRect.width, height: metaRect.height, }; break; default: break; } focusNodeWindow.rect = layout; if (processGap) { focusNodeWindow.rect = this.tree.processGap(focusNodeWindow); } if (!focusNodeWindow.isFloat()) { this.addFloatOverride(focusNodeWindow.nodeValue, false); } this.move(focusNodeWindow.nodeValue, focusNodeWindow.rect); this.queueEvent({ name: "snap-layout-move", callback: () => { this.renderTree("snap-layout-move"); }, }); break; } case "ShowTabDecorationToggle": if (!focusNodeWindow) return; if (!this.ext.settings.get_boolean("tabbed-tiling-mode-enabled")) return; let showTabs = this.ext.settings.get_boolean("showtab-decoration-enabled"); this.ext.settings.set_boolean("showtab-decoration-enabled", !showTabs); this.unfreezeRender(); this.tree.attachNode = focusNodeWindow.parentNode; this.renderTree("showtab-decoration-enabled"); break; case "WindowResizeRight": this.resize(Meta.GrabOp.KEYBOARD_RESIZING_E, action.amount); break; case "WindowResizeLeft": this.resize(Meta.GrabOp.KEYBOARD_RESIZING_W, action.amount); break; case "WindowResizeTop": this.resize(Meta.GrabOp.KEYBOARD_RESIZING_N, action.amount); break; case "WindowResizeBottom": this.resize(Meta.GrabOp.KEYBOARD_RESIZING_S, action.amount); break; default: break; } } resize(grabOp, amount) { let metaWindow = this.focusMetaWindow; let display = global.display; this._handleGrabOpBegin(display, metaWindow, grabOp); let rect = metaWindow.get_frame_rect(); let direction = Utils.directionFromGrab(grabOp); switch (direction) { case Meta.MotionDirection.RIGHT: rect.width = rect.width + amount; break; case Meta.MotionDirection.LEFT: rect.width = rect.width + amount; rect.x = rect.x - amount; break; case Meta.MotionDirection.UP: rect.height = rect.height + amount; break; case Meta.MotionDirection.DOWN: rect.height = rect.height + amount; rect.y = rect.y - amount; break; } this.move(metaWindow, rect); this.queueEvent( { name: "manual-resize", callback: () => { if (this.eventQueue.length === 0) { this._handleGrabOpEnd(display, metaWindow, grabOp); } }, }, 50 ); } disable() { Utils._disableDecorations(); this._removeSignals(); this.disabled = true; Logger.debug(`extension:disable`); } enable() { this._bindSignals(); this.reloadTree("enable"); Logger.debug(`extension:enable`); } findNodeWindow(metaWindow) { return this.tree.findNode(metaWindow); } get focusMetaWindow() { return global.display.get_focus_window(); } get tree() { if (!this._tree) { this._tree = new Tree(this); } return this._tree; } get kbd() { if (!this._kbd) { this._kbd = new Keybindings(this.ext); this.ext.keybindings = this._kbd; } return this._kbd; } get windowsActiveWorkspace() { let wsManager = global.workspace_manager; return global.display.get_tab_list(Meta.TabList.NORMAL_ALL, wsManager.get_active_workspace()); } get windowsAllWorkspaces() { let wsManager = global.workspace_manager; let windowsAll = []; for (let i = 0; i < wsManager.get_n_workspaces(); i++) { Array.prototype.push.apply( windowsAll, global.display.get_tab_list(Meta.TabList.NORMAL_ALL, wsManager.get_workspace_by_index(i)) ); } windowsAll.sort((w1, w2) => { return w1.get_stable_sequence() - w2.get_stable_sequence(); }); return windowsAll; } getWindowsOnWorkspace(workspaceIndex) { const workspaceNode = this.tree.findNode(`ws${workspaceIndex}`); const workspaceWindows = workspaceNode.getNodeByType(NODE_TYPES.WINDOW); return workspaceWindows; } determineSplitLayout() { // if the monitor width is less than height, the monitor could be vertical orientation; let monitorRect = global.display.get_monitor_geometry(global.display.get_current_monitor()); if (monitorRect.width < monitorRect.height) { return LAYOUT_TYPES.VSPLIT; } return LAYOUT_TYPES.HSPLIT; } floatWorkspace(workspaceIndex) { const workspaceWindows = this.getWindowsOnWorkspace(workspaceIndex); if (!workspaceWindows) return; workspaceWindows.forEach((w) => { w.float = true; }); } unfloatWorkspace(workspaceIndex) { const workspaceWindows = this.getWindowsOnWorkspace(workspaceIndex); if (!workspaceWindows) return; workspaceWindows.forEach((w) => { w.tile = true; }); } hideWindowBorders() { this.tree.nodeWindows.forEach((nodeWindow) => { let actor = nodeWindow.windowActor; if (actor) { if (actor.border) { actor.border.hide(); } if (actor.splitBorder) { actor.splitBorder.hide(); } } if (nodeWindow.parentNode.isTabbed()) { if (nodeWindow.tab) { // TODO: review the cleanup of the tab:St.Widget variable try { nodeWindow.tab.remove_style_class_name("window-tabbed-tab-active"); } catch (e) { // Logger.warn(e); } } } }); } // Window movement API move(metaWindow, rect) { if (!metaWindow) return; if (metaWindow.grabbed) return; metaWindow.unmaximize(Meta.MaximizeFlags.HORIZONTAL); metaWindow.unmaximize(Meta.MaximizeFlags.VERTICAL); metaWindow.unmaximize(Meta.MaximizeFlags.BOTH); let windowActor = metaWindow.get_compositor_private(); if (!windowActor) return; windowActor.remove_all_transitions(); metaWindow.move_frame(true, rect.x, rect.y); metaWindow.move_resize_frame(true, rect.x, rect.y, rect.width, rect.height); } moveCenter(metaWindow) { if (!metaWindow) return; let frameRect = metaWindow.get_frame_rect(); const rectRequest = { x: "center", y: "center", width: frameRect.width, height: frameRect.height, }; let moveRect = { x: Utils.resolveX(rectRequest, metaWindow), y: Utils.resolveY(rectRequest, metaWindow), width: Utils.resolveWidth(rectRequest, metaWindow), height: Utils.resolveHeight(rectRequest, metaWindow), }; this.move(metaWindow, moveRect); } rectForMonitor(node, targetMonitor) { if (!node || (node && node.nodeType !== NODE_TYPES.WINDOW)) return null; if (targetMonitor < 0) return null; let currentWorkArea = node.nodeValue.get_work_area_current_monitor(); let nextWorkArea = node.nodeValue.get_work_area_for_monitor(targetMonitor); if (currentWorkArea && nextWorkArea) { let rect = node.rect; if (!rect && node.mode === WINDOW_MODES.FLOAT) { rect = node.nodeValue.get_frame_rect(); } let hRatio = 1; let wRatio = 1; hRatio = nextWorkArea.height / currentWorkArea.height; wRatio = nextWorkArea.width / currentWorkArea.width; rect.height *= hRatio; rect.width *= wRatio; if (nextWorkArea.y < currentWorkArea.y) { rect.y = ((nextWorkArea.y + rect.y - currentWorkArea.y) / currentWorkArea.height) * nextWorkArea.height; } else if (nextWorkArea.y > currentWorkArea.y) { rect.y = (rect.y / currentWorkArea.height) * nextWorkArea.height + nextWorkArea.y; } if (nextWorkArea.x < currentWorkArea.x) { rect.x = ((nextWorkArea.x + rect.x - currentWorkArea.x) / currentWorkArea.width) * nextWorkArea.width; } else if (nextWorkArea.x > currentWorkArea.x) { rect.x = (rect.x / currentWorkArea.width) * nextWorkArea.width + nextWorkArea.x; } return rect; } return null; } _removeSignals() { if (!this._signalsBound) return; if (this._displaySignals) { for (const displaySignal of this._displaySignals) { global.display.disconnect(displaySignal); } this._displaySignals.length = 0; this._displaySignals = undefined; } if (this._windowManagerSignals) { for (const windowManagerSignal of this._windowManagerSignals) { global.window_manager.disconnect(windowManagerSignal); } this._windowManagerSignals.length = 0; this._windowManagerSignals = undefined; } const globalWsm = global.workspace_manager; if (this._workspaceManagerSignals) { for (const workspaceManagerSignal of this._workspaceManagerSignals) { globalWsm.disconnect(workspaceManagerSignal); } this._workspaceManagerSignals.length = 0; this._workspaceManagerSignals = undefined; } let numberOfWorkspaces = globalWsm.get_n_workspaces(); for (let i = 0; i < numberOfWorkspaces; i++) { let workspace = globalWsm.get_workspace_by_index(i); if (workspace.workspaceSignals) { for (const workspaceSignal of workspace.workspaceSignals) { workspace.disconnect(workspaceSignal); } workspace.workspaceSignals.length = 0; workspace.workspaceSignals = undefined; } } let allWindows = this.windowsAllWorkspaces; if (allWindows) { for (let metaWindow of allWindows) { if (metaWindow.windowSignals !== undefined) { for (const windowSignal of metaWindow.windowSignals) { metaWindow.disconnect(windowSignal); } metaWindow.windowSignals.length = 0; metaWindow.windowSignals = undefined; } let windowActor = metaWindow.get_compositor_private(); if (windowActor && windowActor.actorSignals) { for (const actorSignal of windowActor.actorSignals) { windowActor.disconnect(actorSignal); } windowActor.actorSignals.length = 0; windowActor.actorSignals = undefined; } if (windowActor && windowActor.border) { windowActor.border.hide(); if (global.window_group) { global.window_group.remove_child(windowActor.border); } windowActor.border = undefined; } if (windowActor && windowActor.splitBorder) { windowActor.splitBorder.hide(); if (global.window_group) { global.window_group.remove_child(windowActor.splitBorder); } windowActor.splitBorder = undefined; } } } if (this._renderTreeSrcId) { GLib.Source.remove(this._renderTreeSrcId); this._renderTreeSrcId = 0; } if (this._reloadTreeSrcId) { GLib.Source.remove(this._reloadTreeSrcId); this._reloadTreeSrcId = 0; } if (this._wsWindowAddSrcId) { GLib.Source.remove(this._wsWindowAddSrcId); this._wsWindowAddSrcId = 0; } if (this._queueSourceId) { GLib.Source.remove(this._queueSourceId); this._queueSourceId = 0; } if (this._prefsOpenSrcId) { GLib.Source.remove(this._prefsOpenSrcId); this._prefsOpenSrcId = 0; } if (this._overviewSignals) { for (const overviewSignal of this._overviewSignals) { Main.overview.disconnect(overviewSignal); } this._overviewSignals.length = 0; this._overviewSignals = null; } this._signalsBound = false; } renderTree(from, force = false) { let wasFrozen = this._freezeRender; if (force && wasFrozen) this.unfreezeRender(); if (this._freezeRender || !this.ext.settings.get_boolean("tiling-mode-enabled")) { this.updateDecorationLayout(); this.updateBorderLayout(); } else { if (!this._renderTreeSrcId) { this._renderTreeSrcId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { this.processFloats(); this.tree.render(from); this._renderTreeSrcId = 0; this.updateDecorationLayout(); this.updateBorderLayout(); if (wasFrozen) this.freezeRender(); return false; }); } } } processFloats() { this.allNodeWindows.forEach((nodeWindow) => { let metaWindow = nodeWindow.nodeValue; if (this.isFloatingExempt(metaWindow) || !this.isActiveWindowWorkspaceTiled(metaWindow)) { nodeWindow.float = true; } else { nodeWindow.float = false; } }); } get allNodeWindows() { return this.tree.getNodeByType(NODE_TYPES.WINDOW); } /** * Reloads the tree. This is an expensive operation. * Useful when using dynamic workspaces in GNOME-shell. * * TODO: add support to reload the tree from a JSON dump file. * TODO: move this to tree.js */ reloadTree(from) { if (!this._reloadTreeSrcId) { this._reloadTreeSrcId = GLib.idle_add(GLib.PRIORITY_LOW, () => { Utils._disableDecorations(); let treeWorkspaces = this.tree.nodeWorkpaces; let wsManager = global.workspace_manager; let globalWsNum = wsManager.get_n_workspaces(); // empty out the root children nodes this.tree.childNodes.length = 0; this.tree.attachNode = undefined; // initialize the workspaces and monitors id strings this.tree._initWorkspaces(); this.trackCurrentWindows(); this.renderTree(from); this._reloadTreeSrcId = 0; return false; }); } } sameParentMonitor(firstNode, secondNode) { if (!firstNode || !secondNode) return false; if (!firstNode.nodeValue || !secondNode.nodeValue) return false; if (!firstNode.nodeValue.get_workspace()) return false; if (!secondNode.nodeValue.get_workspace()) return false; let firstMonWs = `mo${firstNode.nodeValue.get_monitor()}ws${firstNode.nodeValue .get_workspace() .index()}`; let secondMonWs = `mo${secondNode.nodeValue.get_monitor()}ws${secondNode.nodeValue .get_workspace() .index()}`; return firstMonWs === secondMonWs; } showWindowBorders() { let metaWindow = this.focusMetaWindow; if (!metaWindow) return; let windowActor = metaWindow.get_compositor_private(); if (!windowActor) return; let nodeWindow = this.findNodeWindow(metaWindow); if (!nodeWindow) return; if (metaWindow.get_wm_class() === null) return; let borders = []; let focusBorderEnabled = this.ext.settings.get_boolean("focus-border-toggle"); let splitBorderEnabled = this.ext.settings.get_boolean("split-border-toggle"); let tilingModeEnabled = this.ext.settings.get_boolean("tiling-mode-enabled"); let gap = this.calculateGaps(nodeWindow); let maximized = () => { return metaWindow.get_maximized() === 3 || metaWindow.is_fullscreen() || gap === 0; }; let monitorCount = global.display.get_n_monitors(); let tiledChildren = this.tree.getTiledChildren(nodeWindow.parentNode.childNodes); let inset = 3; let parentNode = nodeWindow.parentNode; const floatingWindow = nodeWindow.isFloat(); const tiledBorder = windowActor.border; if (parentNode.isTabbed()) { if (nodeWindow.tab) { nodeWindow.tab.add_style_class_name("window-tabbed-tab-active"); } } if (tiledBorder && focusBorderEnabled) { if ( !maximized() || (gap === 0 && tiledChildren.length === 1 && monitorCount > 1) || (gap === 0 && tiledChildren.length > 1) ) { if (tilingModeEnabled) { if (parentNode.isStacked()) { if (!floatingWindow) { tiledBorder.set_style_class_name("window-stacked-border"); } else { tiledBorder.set_style_class_name("window-floated-border"); } } else if (parentNode.isTabbed()) { if (!floatingWindow) { tiledBorder.set_style_class_name("window-tabbed-border"); if (nodeWindow.backgroundTab) { tiledBorder.add_style_class_name("window-tabbed-bg"); } } else { tiledBorder.set_style_class_name("window-floated-border"); } } else { if (!floatingWindow) { tiledBorder.set_style_class_name("window-tiled-border"); } else { tiledBorder.set_style_class_name("window-floated-border"); } } } else { tiledBorder.set_style_class_name("window-floated-border"); } borders.push(tiledBorder); } } if (gap === 0 || metaWindow.get_maximized() === 1 || metaWindow.get_maximized() === 2) { inset = 0; } // handle the split border // It should only show when V or H-Split and with single child CONs if ( splitBorderEnabled && focusBorderEnabled && tilingModeEnabled && !nodeWindow.isFloat() && !maximized && parentNode.childNodes.length === 1 && (parentNode.isCon() || parentNode.isMonitor()) && !(parentNode.isTabbed() || parentNode.isStacked()) ) { if (!windowActor.splitBorder) { let splitBorder = new St.Bin({ style_class: "window-split-border" }); global.window_group.add_child(splitBorder); windowActor.splitBorder = splitBorder; } let splitBorder = windowActor.splitBorder; splitBorder.remove_style_class_name("window-split-vertical"); splitBorder.remove_style_class_name("window-split-horizontal"); if (parentNode.isVSplit()) { splitBorder.add_style_class_name("window-split-vertical"); } else if (parentNode.isHSplit()) { splitBorder.add_style_class_name("window-split-horizontal"); } borders.push(splitBorder); } let rect = metaWindow.get_frame_rect(); borders.forEach((border) => { border.set_size(rect.width + inset * 2, rect.height + inset * 2); border.set_position(rect.x - inset, rect.y - inset); if (metaWindow.appears_focused && !metaWindow.minimized) { border.show(); } if (global.window_group && global.window_group.contains(border)) { // TODO - sort the borders with split border being on top global.window_group.remove_child(border); // Add the border just above the focused window global.window_group.insert_child_above(border, metaWindow.get_compositor_private()); } }); } updateBorderLayout() { this.hideWindowBorders(); this.showWindowBorders(); } calculateGaps(node) { if (!node) return 0; let settings = this.ext.settings; let gapSize = settings.get_uint("window-gap-size"); let gapIncrement = settings.get_uint("window-gap-size-increment"); let gap = gapSize * gapIncrement; if (!node.isRoot()) { let hideGapWhenSingle = settings.get_boolean("window-gap-hidden-on-single"); let parentNode = this.tree.findParent(node, NODE_TYPES.MONITOR); if (parentNode) { let tiled = parentNode .getNodeByMode(WINDOW_MODES.TILE) .filter((t) => t.isWindow() && !t.nodeValue.minimized); if (tiled.length == 1 && hideGapWhenSingle) gap = 0; } } return gap; } /** * Track meta/mutter windows and append them to the tree. * Windows can be attached on any of the following Node Types: * MONITOR, CONTAINER * */ trackWindow(_display, metaWindow) { let autoSplit = this.ext.settings.get_boolean("auto-split-enabled"); if (autoSplit && this.focusMetaWindow) { let currentFocusNode = this.tree.findNode(this.focusMetaWindow); if (currentFocusNode) { let currentParentFocusNode = currentFocusNode.parentNode; let layout = currentParentFocusNode.layout; if (layout === LAYOUT_TYPES.HSPLIT || layout === LAYOUT_TYPES.VSPLIT) { let frameRect = this.focusMetaWindow.get_frame_rect(); let splitHorizontal = frameRect.width > frameRect.height; let orientation = splitHorizontal ? "horizontal" : "vertical"; this.command({ name: "Split", orientation: orientation }); } } } // Make window types configurable if (this._validWindow(metaWindow)) { let existNodeWindow = this.tree.findNode(metaWindow); Logger.debug(`Meta Window ${metaWindow.get_title()} ${metaWindow.get_window_type()}`); if (!existNodeWindow) { let attachTarget; const activeMonitor = global.display.get_current_monitor(); const activeWorkspace = global.display.get_workspace_manager().get_active_workspace_index(); let metaMonWs = `mo${activeMonitor}ws${activeWorkspace}`; // Check if the active monitor / workspace has windows let metaMonWsNode = this.tree.findNode(metaMonWs); if (!metaMonWsNode) { // Reload the tree as a last resort this.reloadTree("no-meta-monws"); return; } let windowNodes = metaMonWsNode.getNodeByType(NODE_TYPES.WINDOW); let hasWindows = windowNodes.length > 0; attachTarget = this.tree.attachNode; attachTarget = attachTarget ? this.tree.findNode(attachTarget.nodeValue) : null; if (!attachTarget) { attachTarget = metaMonWsNode; } else { if (hasWindows) { if (attachTarget && metaMonWsNode.contains(attachTarget)) { // Use the attach target } else { // Find the first window attachTarget = windowNodes[0]; } } else { attachTarget = metaMonWsNode; } } let nodeWindow = this.tree.createNode( attachTarget.nodeValue, NODE_TYPES.WINDOW, metaWindow, WINDOW_MODES.FLOAT ); metaWindow.firstRender = true; let windowActor = metaWindow.get_compositor_private(); if (!metaWindow.windowSignals) { let windowSignals = [ metaWindow.connect("position-changed", (_metaWindow) => { let from = "position-changed"; this.updateMetaPositionSize(_metaWindow, from); }), metaWindow.connect("size-changed", (_metaWindow) => { let from = "size-changed"; this.updateMetaPositionSize(_metaWindow, from); }), metaWindow.connect("focus", (_metaWindowFocus) => { this.queueEvent({ name: "focus-update", callback: () => { this.unfreezeRender(); this.updateBorderLayout(); this.updateDecorationLayout(); this.updateStackedFocus(); this.updateTabbedFocus(); let focusNodeWindow = this.tree.findNode(this.focusMetaWindow); this.movePointerWith(focusNodeWindow); }, }); let focusNodeWindow = this.tree.findNode(this.focusMetaWindow); if (focusNodeWindow) { // handle the attach node this.tree.attachNode = focusNodeWindow._parent; if (this.floatingWindow(focusNodeWindow)) { this.queueEvent({ name: "raise-float", callback: () => { this.renderTree("raise-float-queue"); }, }); } this.tree.attachNode = focusNodeWindow; } this.renderTree("focus", true); }), metaWindow.connect("workspace-changed", (_metaWindow) => { this.updateMetaWorkspaceMonitor("metawindow-workspace-changed", null, _metaWindow); this.trackCurrentMonWs(); }), ]; metaWindow.windowSignals = windowSignals; } if (!windowActor.actorSignals) { let actorSignals = [windowActor.connect("destroy", this.windowDestroy.bind(this))]; windowActor.actorSignals = actorSignals; } if (!windowActor.border) { let border = new St.Bin({ style_class: "window-tiled-border" }); if (global.window_group) global.window_group.add_child(border); windowActor.border = border; border.show(); } this.postProcessWindow(nodeWindow); this.queueEvent( { name: "window-create-queue", callback: () => { metaWindow.unmaximize(Meta.MaximizeFlags.HORIZONTAL); metaWindow.unmaximize(Meta.MaximizeFlags.VERTICAL); metaWindow.unmaximize(Meta.MaximizeFlags.BOTH); this.renderTree("window-create", true); }, }, 200 ); let childNodes = this.tree.getTiledChildren(nodeWindow.parentNode.childNodes); childNodes.forEach((n) => { n.percent = 0.0; }); } } } postProcessWindow(nodeWindow) { let metaWindow = nodeWindow.nodeValue; if (metaWindow) { if (metaWindow.get_title() === this.prefsTitle) { metaWindow .get_workspace() .activate_with_focus(metaWindow, global.display.get_current_time()); this.moveCenter(metaWindow); } else { this.movePointerWith(metaWindow); } } } updateStackedFocus(focusNodeWindow) { if (!focusNodeWindow) return; const parentNode = focusNodeWindow.parentNode; if (parentNode.layout === LAYOUT_TYPES.STACKED && !this._freezeRender) { parentNode.appendChild(focusNodeWindow); parentNode.childNodes .filter((child) => child.isWindow()) .forEach((child) => child.nodeValue.raise()); this.queueEvent({ name: "render-focus-stack", callback: () => { this.renderTree("focus-stacked"); }, }); } } updateTabbedFocus(focusNodeWindow) { if (!focusNodeWindow) return; if (focusNodeWindow.parentNode.layout === LAYOUT_TYPES.TABBED && !this._freezeRender) { const metaWindow = focusNodeWindow.nodeValue; metaWindow.raise(); } } /** * Check if a Meta Window's workspace is skipped for tiling. */ isActiveWindowWorkspaceTiled(metaWindow) { if (!metaWindow) return true; let skipWs = this.ext.settings.get_string("workspace-skip-tile"); let skipArr = skipWs.split(","); let skipThisWs = false; for (let i = 0; i < skipArr.length; i++) { let activeWorkspaceForWin = metaWindow.get_workspace(); if (activeWorkspaceForWin) { let wsIndex = activeWorkspaceForWin.index(); if (skipArr[i].trim() === `${wsIndex}`) { skipThisWs = true; break; } } } return !skipThisWs; } /** * Check the current active workspace's tiling mode */ isCurrentWorkspaceTiled() { let skipWs = this.ext.settings.get_string("workspace-skip-tile"); let skipArr = skipWs.split(","); let skipThisWs = false; let wsMgr = global.workspace_manager; let wsIndex = wsMgr.get_active_workspace_index(); for (let i = 0; i < skipArr.length; i++) { if (skipArr[i].trim() === `${wsIndex}`) { skipThisWs = true; break; } } return !skipThisWs; } trackCurrentWindows() { this.tree.attachNode = null; let windowsAll = this.windowsAllWorkspaces; for (let i = 0; i < windowsAll.length; i++) { let metaWindow = windowsAll[i]; this.trackWindow(global.display, metaWindow); // This updates and handles dynamic workspaces this.updateMetaWorkspaceMonitor( "track-current-windows", metaWindow.get_monitor(), metaWindow ); } this.updateDecorationLayout(); } _validWindow(metaWindow) { let windowType = metaWindow.get_window_type(); return ( windowType === Meta.WindowType.NORMAL || windowType === Meta.WindowType.MODAL_DIALOG || windowType === Meta.WindowType.DIALOG ); } windowDestroy(actor) { // Release any resources on the window let border = actor.border; if (border) { if (global.window_group) { global.window_group.remove_child(border); border.hide(); border = null; } } let splitBorder = actor.splitBorder; if (splitBorder) { if (global.window_group) { global.window_group.remove_child(splitBorder); splitBorder.hide(); splitBorder = null; } } let nodeWindow; nodeWindow = this.tree.findNodeByActor(actor); if (nodeWindow?.isWindow()) { this.tree.removeNode(nodeWindow); this.renderTree("window-destroy-quick", true); this.removeFloatOverride(nodeWindow.nodeValue, true); } // find the next attachNode here let focusNodeWindow = this.tree.findNode(this.focusMetaWindow); if (focusNodeWindow) { this.tree.attachNode = focusNodeWindow.parentNode; } this.queueEvent({ name: "window-destroy", callback: () => { this.renderTree("window-destroy", true); }, }); } /** * Handles any workspace/monitor update for the Meta.Window. */ updateMetaWorkspaceMonitor(from, _monitor, metaWindow) { if (this._validWindow(metaWindow)) { if (metaWindow.get_workspace() === null) return; let existNodeWindow = this.tree.findNode(metaWindow); let metaMonWs = `mo${metaWindow.get_monitor()}ws${metaWindow.get_workspace().index()}`; let metaMonWsNode = this.tree.findNode(metaMonWs); if (existNodeWindow) { if (existNodeWindow.parentNode && metaMonWsNode) { // Uses the existing workspace, monitor that the metaWindow // belongs to. let containsWindow = metaMonWsNode.contains(existNodeWindow); if (!containsWindow) { // handle cleanup of resize percentages let existParent = existNodeWindow.parentNode; this.tree.resetSiblingPercent(existParent); metaMonWsNode.appendChild(existNodeWindow); // Ensure that the workspace tiling is honored if (this.isActiveWindowWorkspaceTiled(metaWindow)) { if (!this.grabOp === Meta.GrabOp.WINDOW_BASE) this.updateTabbedFocus(existNodeWindow); this.updateStackedFocus(existNodeWindow); } else { if (this.floatingWindow(existNodeWindow)) { existNodeWindow.nodeValue.raise(); } } } } } this.renderTree(from); } } /** * Handle any updates to the current focused window's position. * Useful for updating the active window border, etc. */ updateMetaPositionSize(_metaWindow, from) { let focusMetaWindow = this.focusMetaWindow; if (!focusMetaWindow) return; let focusNodeWindow = this.findNodeWindow(focusMetaWindow); if (!focusNodeWindow) return; let tilingModeEnabled = this.ext.settings.get_boolean("tiling-mode-enabled"); if (focusNodeWindow.grabMode && tilingModeEnabled) { if (focusNodeWindow.grabMode === GRAB_TYPES.RESIZING) { this._handleResizing(focusNodeWindow); } else if (focusNodeWindow.grabMode === GRAB_TYPES.MOVING) { this._handleMoving(focusNodeWindow); } } else { if (focusMetaWindow.get_maximized() === 0) { this.renderTree(from); } } this.updateBorderLayout(); this.updateDecorationLayout(); } updateDecorationLayout() { if (this._freezeRender) return; let activeWsNode = this.currentWsNode; let allCons = this.tree.getNodeByType(NODE_TYPES.CON); // First, hide all decorations: allCons.forEach((con) => { if (con.decoration) { con.decoration.hide(); } }); // Next, handle showing-desktop usually by Super + D if (!activeWsNode) return; let allWindows = activeWsNode.getNodeByType(NODE_TYPES.WINDOW); let allHiddenWindows = allWindows.filter((w) => { let metaWindow = w.nodeValue; return !metaWindow.showing_on_its_workspace() || metaWindow.minimized; }); // Then if all hidden, do not proceed showing the decorations at all; if (allWindows.length === allHiddenWindows.length) return; // Show the decoration where on all monitors of active workspace // But not on the monitor where there is a maximized or fullscreen window // Note, that when multi-display, user can have multi maximized windows, // So it needs to be fully filtered: let monWsNoMaxWindows = activeWsNode.getNodeByType(NODE_TYPES.MONITOR).filter((monitor) => { return ( monitor.getNodeByType(NODE_TYPES.WINDOW).filter((w) => { return ( w.nodeValue.get_maximized() === Meta.MaximizeFlags.BOTH || w.nodeValue.is_fullscreen() ); }).length === 0 ); }); monWsNoMaxWindows.forEach((monitorWs) => { let activeMonWsCons = monitorWs.getNodeByType(NODE_TYPES.CON); activeMonWsCons.forEach((con) => { let tiled = this.tree.getTiledChildren(con.childNodes); let showTabs = this.ext.settings.get_boolean("showtab-decoration-enabled"); if (con.decoration && tiled.length > 0 && showTabs) { con.decoration.show(); if (global.window_group.contains(con.decoration) && this.focusMetaWindow) { global.window_group.remove_child(con.decoration); // Show it below the focused window global.window_group.insert_child_below( con.decoration, this.focusMetaWindow.get_compositor_private() ); } con.childNodes.forEach((cn) => { cn.render(); }); } }); }); } freezeRender() { this._freezeRender = true; } unfreezeRender() { this._freezeRender = false; } floatingWindow(node) { if (!node) return false; return node.nodeType === NODE_TYPES.WINDOW && node.mode === WINDOW_MODES.FLOAT; } /** * Moves the pointer along with the nodeWindow's meta * * This is useful for making sure that Forge calculates the attachNode * properly */ movePointerWith(nodeWindow) { if (!nodeWindow || !nodeWindow._data) return; if (this.ext.settings.get_boolean("move-pointer-focus-enabled")) { this.storePointerLastPosition(this.lastFocusedWindow); if (this.canMovePointerInsideNodeWindow(nodeWindow)) { this.warpPointerToNodeWindow(nodeWindow); } } this.lastFocusedWindow = nodeWindow; this.tree.debugParentNodes(nodeWindow); } warpPointerToNodeWindow(nodeWindow) { const newCoord = this.getPointerPositionInside(nodeWindow); if (newCoord && newCoord.x && newCoord.y) { const seat = Clutter.get_default_backend().get_default_seat(); if (seat) { const wmTitle = nodeWindow.nodeValue.get_title(); Logger.debug(`moved pointer to [${wmTitle}] at (${newCoord.x},${newCoord.y})`); seat.warp_pointer(newCoord.x, newCoord.y); } } } getPointer() { return global.get_pointer(); } minimizedWindow(node) { if (!node) return false; return node._type === NODE_TYPES.WINDOW && node._data && node._data.minimized; } swapWindowsUnderPointer(focusNodeWindow) { if (this.cancelGrab) { return; } let nodeWinAtPointer = this.findNodeWindowAtPointer(focusNodeWindow); if (nodeWinAtPointer) this.tree.swapPairs(focusNodeWindow, nodeWinAtPointer); } /** * * Handle previewing and applying where a drag-drop window is going to be tiled * */ moveWindowToPointer(focusNodeWindow, preview = false) { if (this.cancelGrab) { return; } if (!focusNodeWindow || focusNodeWindow.mode !== WINDOW_MODES.GRAB_TILE) return; let nodeWinAtPointer = this.nodeWinAtPointer; if (nodeWinAtPointer) { const targetRect = nodeWinAtPointer.nodeValue.get_frame_rect(); const parentNodeTarget = nodeWinAtPointer.parentNode; const currPointer = this.getPointer(); const horizontal = parentNodeTarget.isHSplit() || parentNodeTarget.isTabbed(); const isMonParent = parentNodeTarget.nodeType === NODE_TYPES.MONITOR; const isConParent = parentNodeTarget.nodeType === NODE_TYPES.CON; const centerLayout = this.ext.settings.get_string("dnd-center-layout").toUpperCase(); const stacked = parentNodeTarget.isStacked(); const tabbed = parentNodeTarget.isTabbed(); const stackedOrTabbed = stacked || tabbed; const updatePreview = (focusNodeWindow, previewParams) => { let previewHint = focusNodeWindow.previewHint; let previewHintEnabled = this.ext.settings.get_boolean("preview-hint-enabled"); const previewRect = previewParams.targetRect; if (previewHint && previewHintEnabled) { if (!previewRect) { previewHint.hide(); return; } previewHint.set_style_class_name(previewParams.className); previewHint.set_position(previewRect.x, previewRect.y); previewHint.set_size(previewRect.width, previewRect.height); previewHint.show(); } }; const regions = (targetRect, regionWidth) => { leftRegion = { x: targetRect.x, y: targetRect.y, width: targetRect.width * regionWidth, height: targetRect.height, }; rightRegion = { x: targetRect.x + targetRect.width * (1 - regionWidth), y: targetRect.y, width: targetRect.width * regionWidth, height: targetRect.height, }; topRegion = { x: targetRect.x, y: targetRect.y, width: targetRect.width, height: targetRect.height * regionWidth, }; bottomRegion = { x: targetRect.x, y: targetRect.y + targetRect.height * (1 - regionWidth), width: targetRect.width, height: targetRect.height * regionWidth, }; centerRegion = { x: targetRect.x + targetRect.width * regionWidth, y: targetRect.y + targetRect.height * regionWidth, width: targetRect.width - targetRect.width * regionWidth * 2, height: targetRect.height - targetRect.height * regionWidth * 2, }; return { left: leftRegion, right: rightRegion, top: topRegion, bottom: bottomRegion, center: centerRegion, }; }; let referenceNode = null; let containerNode; let childNode = focusNodeWindow; let previewParams = { className: "", targetRect: null, }; let leftRegion; let rightRegion; let topRegion; let bottomRegion; let centerRegion; let previewWidth = 0.5; let hoverWidth = 0.3; // Hover region detects where the pointer is on the target drop window const hoverRegions = regions(targetRect, hoverWidth); // Preview region interprets the hover intersect where the focus window // would go when dropped const previewRegions = regions(targetRect, previewWidth); leftRegion = hoverRegions.left; rightRegion = hoverRegions.right; topRegion = hoverRegions.top; bottomRegion = hoverRegions.bottom; centerRegion = hoverRegions.center; const isLeft = Utils.rectContainsPoint(leftRegion, currPointer); const isRight = Utils.rectContainsPoint(rightRegion, currPointer); const isTop = Utils.rectContainsPoint(topRegion, currPointer); const isBottom = Utils.rectContainsPoint(bottomRegion, currPointer); const isCenter = Utils.rectContainsPoint(centerRegion, currPointer); if (isCenter) { if (centerLayout == "SWAP") { referenceNode = nodeWinAtPointer; previewParams = { targetRect: targetRect, }; } else { if (stackedOrTabbed) { containerNode = parentNodeTarget; referenceNode = null; previewParams = { className: stacked ? "window-tilepreview-stacked" : "window-tilepreview-tabbed", targetRect: targetRect, }; } else { if (isMonParent) { childNode.createCon = true; containerNode = parentNodeTarget; referenceNode = nodeWinAtPointer; previewParams = { targetRect: targetRect, }; } else { containerNode = parentNodeTarget; referenceNode = null; const parentTargetRect = this.tree.processGap(parentNodeTarget); previewParams = { targetRect: parentTargetRect, }; } } } } else if (isLeft) { previewParams = { targetRect: previewRegions.left, }; if (stackedOrTabbed) { // treat any windows on stacked or tabbed layouts to be // a single node unit: the con itself and then // split left, top, right or bottom accordingly (subsequent if conditions): childNode.detachWindow = true; if (!isMonParent) { referenceNode = parentNodeTarget; containerNode = parentNodeTarget.parentNode; } else { // It is a monitor that's a stack/tab // TODO: update the stacked/tabbed toggles to not // change layout if the parent is a monitor? } } else { if (horizontal) { containerNode = parentNodeTarget; referenceNode = nodeWinAtPointer; } else { // vertical orientation childNode.createCon = true; containerNode = parentNodeTarget; referenceNode = nodeWinAtPointer; } } } else if (isRight) { previewParams = { targetRect: previewRegions.right, }; if (stackedOrTabbed) { // treat any windows on stacked or tabbed layouts to be // a single node unit: the con itself and then // split left, top, right or bottom accordingly (subsequent if conditions): childNode.detachWindow = true; if (!isMonParent) { referenceNode = parentNodeTarget.nextSibling; containerNode = parentNodeTarget.parentNode; } else { // It is a monitor that's a stack/tab // TODO: update the stacked/tabbed toggles to not // change layout if the parent is a monitor? } } else { if (horizontal) { containerNode = parentNodeTarget; referenceNode = nodeWinAtPointer.nextSibling; } else { childNode.createCon = true; containerNode = parentNodeTarget; referenceNode = nodeWinAtPointer.nextSibling; } } } else if (isTop) { previewParams = { targetRect: previewRegions.top, }; if (stackedOrTabbed) { // treat any windows on stacked or tabbed layouts to be // a single node unit: the con itself and then // split left, top, right or bottom accordingly (subsequent if conditions): if (!isMonParent) { containerNode = parentNodeTarget; referenceNode = null; previewParams = { className: stacked ? "window-tilepreview-stacked" : "window-tilepreview-tabbed", targetRect: targetRect, }; } else { // It is a monitor that's a stack/tab // TODO: update the stacked/tabbed toggles to not // change layout if the parent is a monitor? } } else { if (horizontal) { childNode.createCon = true; containerNode = parentNodeTarget; referenceNode = nodeWinAtPointer; } else { containerNode = parentNodeTarget; referenceNode = nodeWinAtPointer; } } } else if (isBottom) { previewParams = { targetRect: previewRegions.bottom, }; if (stackedOrTabbed) { // treat any windows on stacked or tabbed layouts to be // a single node unit: the con itself and then // split left, top, right or bottom accordingly (subsequent if conditions): if (!isMonParent) { containerNode = parentNodeTarget; referenceNode = null; previewParams = { className: stacked ? "window-tilepreview-stacked" : "window-tilepreview-tabbed", targetRect: targetRect, }; } else { // It is a monitor that's a stack/tab // TODO: update the stacked/tabbed toggles to not // change layout if the parent is a monitor? } } else { if (horizontal) { childNode = focusNodeWindow; childNode.createCon = true; containerNode = parentNodeTarget; referenceNode = nodeWinAtPointer.nextSibling; } else { childNode = focusNodeWindow; containerNode = parentNodeTarget; referenceNode = nodeWinAtPointer.nextSibling; } } } if (!isCenter) { if (stackedOrTabbed) { if (isLeft || isRight) { previewParams.className = "window-tilepreview-tiled"; } else if (isTop || isBottom) { previewParams.className = stacked ? "window-tilepreview-stacked" : "window-tilepreview-tabbed"; } } else { previewParams.className = "window-tilepreview-tiled"; } } else if (isCenter) { if (!stackedOrTabbed) previewParams.className = this._getDragDropCenterPreviewStyle(); } if (!preview) { const previousParent = focusNodeWindow.parentNode; this.tree.resetSiblingPercent(containerNode); this.tree.resetSiblingPercent(previousParent); if (focusNodeWindow.tab) { let decoParent = focusNodeWindow.tab.get_parent(); if (decoParent) decoParent.remove_child(focusNodeWindow.tab); } if (childNode.createCon) { const numWin = parentNodeTarget.childNodes.filter( (c) => c.nodeType === NODE_TYPES.WINDOW ).length; const numChild = parentNodeTarget.childNodes.length; const sameNumChild = numWin === numChild; // Child Node will still be created if ( !isCenter && ((isConParent && numWin === 1 && sameNumChild) || (isMonParent && numWin == 2 && sameNumChild)) ) { childNode = parentNodeTarget; } else { childNode = new Node(NODE_TYPES.CON, new St.Bin()); containerNode.insertBefore(childNode, referenceNode); childNode.appendChild(nodeWinAtPointer); } if (isLeft || isTop) { childNode.insertBefore(focusNodeWindow, nodeWinAtPointer); } else if (isRight || isBottom || isCenter) { childNode.insertBefore(focusNodeWindow, null); } if (isLeft || isRight) { childNode.layout = LAYOUT_TYPES.HSPLIT; } else if (isTop || isBottom) { childNode.layout = LAYOUT_TYPES.VSPLIT; } else if (isCenter) { childNode.layout = LAYOUT_TYPES[centerLayout]; } } else if (childNode.detachWindow) { const orientation = isLeft || isRight ? ORIENTATION_TYPES.HORIZONTAL : ORIENTATION_TYPES.VERTICAL; this.tree.split(childNode, orientation); containerNode.insertBefore(childNode.parentNode, referenceNode); } else if (isCenter && centerLayout == "SWAP") { this.tree.swapPairs(referenceNode, focusNodeWindow); this.renderTree("drag-swap"); } else { // Child Node is a WINDOW containerNode.insertBefore(childNode, referenceNode); if (isLeft || isRight) { containerNode.layout = LAYOUT_TYPES.HSPLIT; } else if (isTop || isBottom) { if (!stackedOrTabbed) containerNode.layout = LAYOUT_TYPES.VSPLIT; } else if (isCenter) { if (containerNode.isHSplit() || containerNode.isVSplit()) { containerNode.layout = LAYOUT_TYPES[centerLayout]; } } } previousParent.resetLayoutSingleChild(); } else { updatePreview(focusNodeWindow, previewParams); } childNode.createCon = false; childNode.detachWindow = false; } } canMovePointerInsideNodeWindow(nodeWindow) { if (nodeWindow && nodeWindow._data) { const metaWindow = nodeWindow.nodeValue; const metaRect = metaWindow.get_frame_rect(); const pointerCoord = global.get_pointer(); return ( metaRect && // xdg-copy creates a 1x1 pixel window to capture mouse events. metaRect.width > 8 && metaRect.height > 8 && !Utils.rectContainsPoint(metaRect, pointerCoord) && !metaWindow.minimized && !Main.overview.visible && !this.pointerIsOverParentDecoration(nodeWindow, pointerCoord) ); } return false; } pointerIsOverParentDecoration(nodeWindow, pointerCoord) { if (pointerCoord && nodeWindow && nodeWindow.parentNode) { let node = nodeWindow.parentNode; if (node.isTabbed() || node.isStacked()) { return Utils.rectContainsPoint(node.rect, pointerCoord); } } return false; } getPointerPositionInside(nodeWindow) { if (nodeWindow && nodeWindow._data) { const metaWindow = nodeWindow.nodeValue; const metaRect = metaWindow.get_frame_rect(); // on: last position of cursor inside window // on: titlebar: near to app toolbars, menubar, tabs, etc... let [wx, wy] = nodeWindow.pointer ? [nodeWindow.pointer.x, nodeWindow.pointer.y] : [metaRect.width / 2, 8]; let px = wx >= metaRect.width ? metaRect.width - 8 : wx; let py = wy >= metaRect.height ? metaRect.height - 8 : wy; return { x: metaRect.x + px, y: metaRect.y + py, }; } return null; } storePointerLastPosition(nodeWindow) { if (nodeWindow && nodeWindow._data) { const metaWindow = nodeWindow.nodeValue; const metaRect = metaWindow.get_frame_rect(); const pointerCoord = global.get_pointer(); if (Utils.rectContainsPoint(metaRect, pointerCoord)) { let px = pointerCoord[0] - metaRect.x; let py = pointerCoord[1] - metaRect.y; if (px > 0 && py > 0) { nodeWindow.pointer = { x: px, y: py }; Logger.debug(`stored pointer for [${metaWindow.get_title()}] at (${px},${py})`); } } } } findNodeWindowAtPointer(focusNodeWindow) { let pointerCoord = global.get_pointer(); let nodeWinAtPointer = this._findNodeWindowAtPointer(focusNodeWindow.nodeValue, pointerCoord); return nodeWinAtPointer; } /** * Finds the NodeWindow under the Meta.Window and the * current pointer coordinates; */ _findNodeWindowAtPointer(metaWindow, pointer) { if (!metaWindow) return undefined; let sortedWindows = this.sortedWindows; if (!sortedWindows) { Logger.warn("No sorted windows"); return; } for (let i = 0, n = sortedWindows.length; i < n; i++) { const w = sortedWindows[i]; const metaRect = w.get_frame_rect(); const atPointer = Utils.rectContainsPoint(metaRect, pointer); if (atPointer) return this.tree.getNodeByValue(w); } return null; } _handleGrabOpBegin(_display, _metaWindow, grabOp) { this.grabOp = grabOp; this.trackCurrentMonWs(); let focusMetaWindow = this.focusMetaWindow; if (focusMetaWindow) { let focusNodeWindow = this.findNodeWindow(focusMetaWindow); if (!focusNodeWindow) return; const frameRect = focusMetaWindow.get_frame_rect(); const gaps = this.calculateGaps(focusNodeWindow); focusNodeWindow.grabMode = Utils.grabMode(grabOp); if ( focusNodeWindow.grabMode === GRAB_TYPES.MOVING && focusNodeWindow.mode === WINDOW_MODES.TILE ) { this.freezeRender(); focusNodeWindow.mode = WINDOW_MODES.GRAB_TILE; } focusNodeWindow.initGrabOp = grabOp; focusNodeWindow.initRect = Utils.removeGapOnRect(frameRect, gaps); } } _handleGrabOpEnd(_display, _metaWindow, grabOp) { this.unfreezeRender(); let focusMetaWindow = this.focusMetaWindow; if (!focusMetaWindow) return; let focusNodeWindow = this.findNodeWindow(focusMetaWindow); if (focusNodeWindow && !this.cancelGrab) { // WINDOW_BASE is when grabbing the window decoration // COMPOSITOR is when something like Overview requesting a grab, especially when Super is pressed. if ( grabOp === Meta.GrabOp.WINDOW_BASE || grabOp === Meta.GrabOp.COMPOSITOR || grabOp === Meta.GrabOp.MOVING_UNCONSTRAINED ) { if (this.allowDragDropTile()) { this.moveWindowToPointer(focusNodeWindow); } } } this._grabCleanup(focusNodeWindow); if (focusMetaWindow.get_maximized() === 0) { this.renderTree("grab-op-end"); } this.updateStackedFocus(focusNodeWindow); this.updateTabbedFocus(focusNodeWindow); this.nodeWinAtPointer = null; } _grabCleanup(focusNodeWindow) { this.cancelGrab = false; if (!focusNodeWindow) return; focusNodeWindow.initRect = null; focusNodeWindow.grabMode = null; focusNodeWindow.initGrabOp = null; if (focusNodeWindow.previewHint) { focusNodeWindow.previewHint.hide(); global.window_group.remove_child(focusNodeWindow.previewHint); focusNodeWindow.previewHint.destroy(); focusNodeWindow.previewHint = null; } if (focusNodeWindow.mode === WINDOW_MODES.GRAB_TILE) { focusNodeWindow.mode = WINDOW_MODES.TILE; } } allowDragDropTile() { return this.kbd.allowDragDropTile(); } _handleResizing(focusNodeWindow) { if (!focusNodeWindow || focusNodeWindow.isFloat()) return; let grabOps = Utils.decomposeGrabOp(this.grabOp); for (let grabOp of grabOps) { let initGrabOp = focusNodeWindow.initGrabOp; let direction = Utils.directionFromGrab(grabOp); let orientation = Utils.orientationFromGrab(grabOp); let parentNodeForFocus = focusNodeWindow.parentNode; let position = Utils.positionFromGrabOp(grabOp); // normalize the rect without gaps let frameRect = this.focusMetaWindow.get_frame_rect(); let gaps = this.calculateGaps(focusNodeWindow); let currentRect = Utils.removeGapOnRect(frameRect, gaps); let firstRect; let secondRect; let parentRect; let resizePairForWindow; if (initGrabOp === Meta.GrabOp.RESIZING_UNKNOWN) { // the direction is null so do not process yet below. return; } else { resizePairForWindow = this.tree.nextVisible(focusNodeWindow, direction); } let sameParent = resizePairForWindow ? resizePairForWindow.parentNode === focusNodeWindow.parentNode : false; if (orientation === ORIENTATION_TYPES.HORIZONTAL) { if (sameParent) { // use the window or con pairs if (this.tree.getTiledChildren(parentNodeForFocus.childNodes).length <= 1) { return; } firstRect = focusNodeWindow.initRect; if (resizePairForWindow) { if ( !this.floatingWindow(resizePairForWindow) && !this.minimizedWindow(resizePairForWindow) ) { secondRect = resizePairForWindow.rect; } else { // TODO try to get the next resize pair? } } if (!firstRect || !secondRect) { return; } parentRect = parentNodeForFocus.rect; let changePx = currentRect.width - firstRect.width; let firstPercent = (firstRect.width + changePx) / parentRect.width; let secondPercent = (secondRect.width - changePx) / parentRect.width; focusNodeWindow.percent = firstPercent; resizePairForWindow.percent = secondPercent; } else { // use the parent pairs (con to another con or window) if (resizePairForWindow && resizePairForWindow.parentNode) { if (this.tree.getTiledChildren(resizePairForWindow.parentNode.childNodes).length <= 1) { return; } let firstWindowRect = focusNodeWindow.initRect; let index = resizePairForWindow.index; if (position === POSITION.BEFORE) { // Find the opposite node index = index + 1; } else { index = index - 1; } parentNodeForFocus = resizePairForWindow.parentNode.childNodes[index]; firstRect = parentNodeForFocus.rect; secondRect = resizePairForWindow.rect; if (!firstRect || !secondRect) { return; } parentRect = parentNodeForFocus.parentNode.rect; let changePx = currentRect.width - firstWindowRect.width; let firstPercent = (firstRect.width + changePx) / parentRect.width; let secondPercent = (secondRect.width - changePx) / parentRect.width; parentNodeForFocus.percent = firstPercent; resizePairForWindow.percent = secondPercent; } } } else if (orientation === ORIENTATION_TYPES.VERTICAL) { if (sameParent) { // use the window or con pairs if (this.tree.getTiledChildren(parentNodeForFocus.childNodes).length <= 1) { return; } firstRect = focusNodeWindow.initRect; if (resizePairForWindow) { if ( !this.floatingWindow(resizePairForWindow) && !this.minimizedWindow(resizePairForWindow) ) { secondRect = resizePairForWindow.rect; } else { // TODO try to get the next resize pair? } } if (!firstRect || !secondRect) { return; } parentRect = parentNodeForFocus.rect; let changePx = currentRect.height - firstRect.height; let firstPercent = (firstRect.height + changePx) / parentRect.height; let secondPercent = (secondRect.height - changePx) / parentRect.height; focusNodeWindow.percent = firstPercent; resizePairForWindow.percent = secondPercent; } else { // use the parent pairs (con to another con or window) if (resizePairForWindow && resizePairForWindow.parentNode) { if (this.tree.getTiledChildren(resizePairForWindow.parentNode.childNodes).length <= 1) { return; } let firstWindowRect = focusNodeWindow.initRect; let index = resizePairForWindow.index; if (position === POSITION.BEFORE) { // Find the opposite node index = index + 1; } else { index = index - 1; } parentNodeForFocus = resizePairForWindow.parentNode.childNodes[index]; firstRect = parentNodeForFocus.rect; secondRect = resizePairForWindow.rect; if (!firstRect || !secondRect) { return; } parentRect = parentNodeForFocus.parentNode.rect; let changePx = currentRect.height - firstWindowRect.height; let firstPercent = (firstRect.height + changePx) / parentRect.height; let secondPercent = (secondRect.height - changePx) / parentRect.height; parentNodeForFocus.percent = firstPercent; resizePairForWindow.percent = secondPercent; } } } } } _handleMoving(focusNodeWindow) { if (!focusNodeWindow || focusNodeWindow.mode !== WINDOW_MODES.GRAB_TILE) return; const nodeWinAtPointer = this.findNodeWindowAtPointer(focusNodeWindow); this.nodeWinAtPointer = nodeWinAtPointer; const hidePreview = () => { if (focusNodeWindow.previewHint) { focusNodeWindow.previewHint.hide(); } }; if (nodeWinAtPointer) { if (!focusNodeWindow.previewHint) { let previewHint = new St.Bin(); global.window_group.add_child(previewHint); focusNodeWindow.previewHint = previewHint; } if (this.allowDragDropTile()) { this.moveWindowToPointer(focusNodeWindow, true); } else { hidePreview(); } } else { hidePreview(); } } isFloatingExempt(metaWindow) { if (!metaWindow) return true; let windowTitle = metaWindow.get_title(); let windowType = metaWindow.get_window_type(); let floatByType = windowType === Meta.WindowType.DIALOG || windowType === Meta.WindowType.MODAL_DIALOG || metaWindow.get_transient_for() !== null || metaWindow.get_wm_class() === null || windowTitle === null || windowTitle === "" || windowTitle.length === 0 || !metaWindow.allows_resize(); const knownFloats = this.windowProps.overrides.filter((wprop) => wprop.mode === "float"); let floatOverride = knownFloats.filter((kf) => { let matchTitle = false; let matchClass = false; let matchId = false; if (kf.wmTitle) { if (kf.wmTitle === " ") { matchTitle = kf.wmTitle === windowTitle; } else { let titles = kf.wmTitle.split(","); matchTitle = titles.filter((t) => { if (windowTitle) { if (t.startsWith("!")) { return !windowTitle.includes(t.slice(1)); } else { return windowTitle.includes(t); } } return false; }).length > 0; } } if (kf.wmClass) { matchClass = kf.wmClass.includes(metaWindow.get_wm_class()); } if (kf.wmId) { matchId = kf.wmId === metaWindow.get_id(); } return (!kf.wmId || matchId) && (!kf.wmTitle || matchTitle) && matchClass; }).length > 0; return floatByType || floatOverride; } _getDragDropCenterPreviewStyle() { const centerLayout = this.ext.settings.get_string("dnd-center-layout"); return `window-tilepreview-${centerLayout}`; } get currentMonWsNode() { const monWs = this.currentMonWs; if (monWs) { return this.tree.findNode(monWs); } return null; } get currentWsNode() { const ws = this.currentWs; if (ws) { return this.tree.findNode(ws); } return null; } get currentMonWs() { const monWs = `${this.currentMon}${this.currentWs}`; return monWs; } get currentWs() { const display = global.display; const wsMgr = display.get_workspace_manager(); return `ws${wsMgr.get_active_workspace_index()}`; } get currentMon() { const display = global.display; return `mo${display.get_current_monitor()}`; } floatAllWindows() { this.tree.getNodeByType(NODE_TYPES.WINDOW).forEach((w) => { if (w.isFloat()) { w.prevFloat = true; } w.mode = WINDOW_MODES.FLOAT; }); } unfloatAllWindows() { this.tree.getNodeByType(NODE_TYPES.WINDOW).forEach((w) => { if (!w.prevFloat) { w.mode = WINDOW_MODES.TILE; } else { // Reset the float marker w.prevFloat = false; } }); } }