From 59da6a08833e1abf1a10c5c8c4e56be02d06d818 Mon Sep 17 00:00:00 2001 From: Gsantana Date: Sun, 11 Oct 2026 12:54:41 -0300 Subject: [PATCH] fix: prevent gnome-shell crash when closing windows on Wayland --- lib/extension/window.js | 1130 +++++++++++++++++++++++---------------- 1 file changed, 682 insertions(+), 448 deletions(-) diff --git a/lib/extension/window.js b/lib/extension/window.js index 61820e1e..a17a94de 100644 --- a/lib/extension/window.js +++ b/lib/extension/window.js @@ -73,6 +73,7 @@ export class WindowManager extends GObject.Object { this.eventQueue = new Queue(); this.theme = this.ext.theme; this.lastFocusedWindow = null; + this.grabOp = null; this.shouldFocusOnHover = this.ext.settings.get_boolean("focus-on-hover-enabled"); Logger.info("forge initialized"); @@ -84,6 +85,228 @@ export class WindowManager extends GObject.Object { } } + // --------------------------------------------------------------------------- + // Window lifecycle helpers: never call into Mutter for windows that are not + // ready yet (no first frame) or that are being unmanaged. + // --------------------------------------------------------------------------- + + /** + * True while the MetaWindow is still managed and has a compositor actor. + */ + _isAlive(metaWindow) { + if (!metaWindow || metaWindow._forgeDead) return false; + try { + return !!metaWindow.get_compositor_private(); + } catch (e) { + return false; + } + } + + _isMaximized(metaWindow) { + try { + // GNOME 49+ + return metaWindow.is_maximized(); + } catch (e) { + // pre-49 fallback + return metaWindow.get_maximized() !== 0; + } + } + + _unmaximize(metaWindow) { + try { + // GNOME 49+ + metaWindow.set_unmaximize_flags(Meta.MaximizeFlags.BOTH); + metaWindow.unmaximize(); + } catch (e) { + // pre-49 fallback + metaWindow.unmaximize(Meta.MaximizeFlags.BOTH); + } + } + + _markWindowReady(metaWindow, from) { + if (!metaWindow || metaWindow._forgeDead || metaWindow._forgeReady) return; + metaWindow._forgeReady = true; + this.renderTree(`window-ready:${from}`, true); + } + + /** + * Mark the window as ready on whichever comes first: the actor's + * "first-frame", the window's "shown", or a safety timeout (in case + * first-frame was emitted before we connected). + */ + _watchWindowReady(metaWindow, windowActor) { + if (metaWindow._forgeReady || metaWindow._forgeReadyWatch) return; + metaWindow._forgeReadyWatch = true; + + const ids = { ff: 0, shown: 0, timeout: 0 }; + const cleanup = () => { + if (ids.ff) { + try { + windowActor.disconnect(ids.ff); + } catch (e) { + // already disconnected + } + ids.ff = 0; + } + if (ids.shown) { + try { + metaWindow.disconnect(ids.shown); + } catch (e) { + // already disconnected + } + ids.shown = 0; + } + if (ids.timeout) { + GLib.Source.remove(ids.timeout); + ids.timeout = 0; + } + windowActor._forgeFirstFrameId = 0; + metaWindow._forgeReadyWatch = false; + }; + const ready = (from) => { + cleanup(); + this._markWindowReady(metaWindow, from); + }; + + ids.ff = windowActor.connect("first-frame", () => ready("first-frame")); + windowActor._forgeFirstFrameId = ids.ff; + ids.shown = metaWindow.connect("shown", () => ready("shown")); + ids.timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { + ids.timeout = 0; + if (this._isAlive(metaWindow)) ready("timeout"); + else cleanup(); + return GLib.SOURCE_REMOVE; + }); + metaWindow._forgeReadyCleanup = cleanup; + } + + /** + * Wayland: "window-created" is emitted before the compositor actor exists. + * Wait for the actor and then track the window. + */ + _deferTrackWindow(metaWindow) { + if (metaWindow._forgeDeferred) return; + metaWindow._forgeDeferred = true; + + let tries = 0; + let shownId = 0; + let sourceId = 0; + + const finish = () => { + if (shownId) { + try { + metaWindow.disconnect(shownId); + } catch (e) { + // already disconnected + } + shownId = 0; + } + if (sourceId) { + GLib.Source.remove(sourceId); + sourceId = 0; + } + metaWindow._forgeDeferred = false; + }; + + const attempt = () => { + if (this.disabled || metaWindow._forgeDead) { + finish(); + return false; + } + if (metaWindow.get_compositor_private()) { + finish(); + this.trackWindow(global.display, metaWindow); + return false; + } + return true; + }; + + shownId = metaWindow.connect("shown", () => attempt()); + sourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => { + // This source is being dispatched: don't let finish() remove it + const ownId = sourceId; + sourceId = 0; + if (++tries > 100) { + // ~5s without an actor: give up + finish(); + return GLib.SOURCE_REMOVE; + } + const keep = attempt(); + if (keep) sourceId = ownId; + return keep ? GLib.SOURCE_CONTINUE : GLib.SOURCE_REMOVE; + }); + } + + _destroyActorDecorations(actor) { + if (!actor) return; + for (const key of ["border", "splitBorder"]) { + const deco = actor[key]; + if (!deco) continue; + try { + deco.hide(); + const parent = deco.get_parent(); + if (parent) parent.remove_child(deco); + deco.destroy(); + } catch (e) { + Logger.warn(`decoration cleanup: ${e.message}`); + } + actor[key] = undefined; + } + } + + _disconnectWindowSignals(metaWindow) { + if (!metaWindow || !metaWindow.windowSignals) return; + for (const id of metaWindow.windowSignals) { + try { + metaWindow.disconnect(id); + } catch (e) { + // already disconnected + } + } + metaWindow.windowSignals = undefined; + } + + /** + * Called on "unmanaging": the window is still valid here, but from now on + * no move/focus/raise calls may be made on it. Previously the node was only + * removed on the actor's "destroy" (after the close animation), leaving a + * window in the tree that renderTree() kept moving. + */ + _onWindowUnmanaging(metaWindow) { + metaWindow._forgeDead = true; + if (metaWindow._forgeReadyCleanup) { + metaWindow._forgeReadyCleanup(); + metaWindow._forgeReadyCleanup = null; + } + + this._destroyActorDecorations(metaWindow.get_compositor_private()); + this._disconnectWindowSignals(metaWindow); + + const nodeWindow = this.tree.findNode(metaWindow); + if (nodeWindow && nodeWindow.isWindow()) { + const parent = nodeWindow.parentNode; + // The window is still valid during "unmanaging" + this.removeFloatOverride(metaWindow, true); + this.tree.removeNode(nodeWindow); + if (parent) this.tree.resetSiblingPercent(parent); + if (this.lastFocusedWindow === nodeWindow) this.lastFocusedWindow = null; + } + + if (this.sortedWindows) { + this.sortedWindows = this.sortedWindows.filter((w) => w !== metaWindow); + } + if (this.nodeWinAtPointer && this.nodeWinAtPointer.nodeValue === metaWindow) { + this.nodeWinAtPointer = null; + } + + this.queueEvent({ + name: "window-unmanaged", + callback: () => { + this.renderTree("window-unmanaged", true); + }, + }); + } + pointerLoopInit() { if (this._pointerFocusTimeoutId) { GLib.Source.remove(this._pointerFocusTimeoutId); @@ -139,7 +362,7 @@ export class WindowManager extends GObject.Object { toggleFloatingMode(action, metaWindow) { let nodeWindow = this.findNodeWindow(metaWindow); - if (!nodeWindow || !(action || action.mode)) return; + if (!nodeWindow || !action) return; if (nodeWindow.nodeType !== NODE_TYPES.WINDOW) return; let withWmId = action.name === "FloatToggle"; @@ -167,7 +390,11 @@ export class WindowManager extends GObject.Object { this._queueSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => { const currEventObj = this.eventQueue.dequeue(); if (currEventObj) { - currEventObj.callback(); + try { + currEventObj.callback(); + } catch (e) { + Logger.warn(`queued event ${currEventObj.name} failed: ${e.message}`); + } } const result = this.eventQueue.length !== 0; if (!result) { @@ -289,7 +516,7 @@ export class WindowManager extends GObject.Object { let settings = this.ext.settings; - settings.connect("changed", (_, settingName) => { + this._settingsSignal = settings.connect("changed", (_, settingName) => { switch (settingName) { case "window-overrides-reload-trigger": // Reload window overrides when triggered by preferences @@ -394,7 +621,7 @@ export class WindowManager extends GObject.Object { cleanupAlwaysFloat() { // remove the setting for each node window this.allNodeWindows.forEach((w) => { - if (w.mode === WINDOW_MODES.FLOAT) { + if (w.mode === WINDOW_MODES.FLOAT && this._isAlive(w.nodeValue)) { w.nodeValue.is_above() && w.nodeValue.unmake_above(); } }); @@ -402,7 +629,7 @@ export class WindowManager extends GObject.Object { restoreAlwaysFloat() { this.allNodeWindows.forEach((w) => { - if (w.mode === WINDOW_MODES.FLOAT) { + if (w.mode === WINDOW_MODES.FLOAT && this._isAlive(w.nodeValue)) { !w.nodeValue.is_above() && w.nodeValue.make_above(); } }); @@ -410,7 +637,8 @@ export class WindowManager extends GObject.Object { trackCurrentMonWs() { let metaWindow = this.focusMetaWindow; - if (!metaWindow) return; + if (!this._isAlive(metaWindow)) return; + if (!metaWindow.get_workspace()) return; const currentMonitor = global.display.get_current_monitor(); const currentWorkspace = global.display.get_workspace_manager().get_active_workspace_index(); @@ -428,6 +656,7 @@ export class WindowManager extends GObject.Object { .getNodeByType(NODE_TYPES.WINDOW) .filter( (w) => + this._isAlive(w.nodeValue) && !w.nodeValue.minimized && w.isTile() && w.nodeValue !== metaWindow && @@ -449,12 +678,14 @@ export class WindowManager extends GObject.Object { 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; + if (this._isAlive(metaWindow)) { + this.updateMetaWorkspaceMonitor( + "window-added", + metaWindow.get_monitor(), + metaWindow + ); + } return false; }); } @@ -475,7 +706,8 @@ export class WindowManager extends GObject.Object { switch (action.name) { case "FloatNonPersistentToggle": case "FloatToggle": - case "FloatClassToggle": + case "FloatClassToggle": { + if (!focusNodeWindow || !this._isAlive(focusWindow)) return; this.toggleFloatingMode(action, focusWindow); const rectRequest = { @@ -504,7 +736,9 @@ export class WindowManager extends GObject.Object { this.tree.resetSiblingPercent(existParent); this.renderTree("float-toggle", true); break; - case "Move": + } + case "Move": { + if (!focusNodeWindow) return; this.unfreezeRender(); let moveDirection = Utils.resolveDirection(action.direction); let prev = focusNodeWindow; @@ -517,6 +751,7 @@ export class WindowManager extends GObject.Object { callback: () => { if (this.eventQueue.length <= 0) { this.unfreezeRender(); + if (!focusNodeWindow || !this._isAlive(focusNodeWindow.nodeValue)) return; if (focusNodeWindow.parentNode.layout === LAYOUT_TYPES.STACKED) { focusNodeWindow.parentNode.appendChild(focusNodeWindow); focusNodeWindow.nodeValue.raise(); @@ -539,25 +774,28 @@ export class WindowManager extends GObject.Object { } break; - case "Focus": + } + case "Focus": { let focusDirection = Utils.resolveDirection(action.direction); focusNodeWindow = this.tree.focus(focusNodeWindow, focusDirection); if (!focusNodeWindow) { focusNodeWindow = this.findNodeWindow(this.focusMetaWindow); } break; - case "Swap": + } + case "Swap": { if (!focusNodeWindow) return; this.unfreezeRender(); let swapDirection = Utils.resolveDirection(action.direction); this.tree.swap(focusNodeWindow, swapDirection); - focusNodeWindow.nodeValue.raise(); + if (this._isAlive(focusNodeWindow.nodeValue)) focusNodeWindow.nodeValue.raise(); this.updateTabbedFocus(focusNodeWindow); this.updateStackedFocus(focusNodeWindow); this.movePointerWith(focusNodeWindow); this.renderTree("swap", true); break; - case "Split": + } + case "Split": { if (!focusNodeWindow) return; currentLayout = focusNodeWindow.parentNode.layout; if (currentLayout === LAYOUT_TYPES.STACKED || currentLayout === LAYOUT_TYPES.TABBED) { @@ -569,6 +807,7 @@ export class WindowManager extends GObject.Object { this.tree.split(focusNodeWindow, orientation); this.renderTree("split"); break; + } case "LayoutToggle": if (!focusNodeWindow) return; currentLayout = focusNodeWindow.parentNode.layout; @@ -580,11 +819,12 @@ export class WindowManager extends GObject.Object { this.tree.attachNode = focusNodeWindow.parentNode; this.renderTree("layout-split-toggle"); break; - case "FocusBorderToggle": + case "FocusBorderToggle": { let focusBorderEnabled = this.ext.settings.get_boolean("focus-border-toggle"); this.ext.settings.set_boolean("focus-border-toggle", !focusBorderEnabled); break; - case "TilingModeToggle": + } + 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"); @@ -596,7 +836,8 @@ export class WindowManager extends GObject.Object { } this.renderTree(`tiling-mode-toggle ${!tilingModeEnabled}`); break; - case "GapSize": + } + case "GapSize": { let gapIncrement = this.ext.settings.get_uint("window-gap-size-increment"); let amount = action.amount; gapIncrement = gapIncrement + amount; @@ -604,7 +845,8 @@ export class WindowManager extends GObject.Object { if (gapIncrement > 8) gapIncrement = 8; this.ext.settings.set_uint("window-gap-size-increment", gapIncrement); break; - case "WorkspaceActiveTileToggle": + } + case "WorkspaceActiveTileToggle": { let activeWorkspace = global.workspace_manager.get_active_workspace_index(); let skippedWorkspaces = this.ext.settings.get_string("workspace-skip-tile"); let workspaceSkipped = false; @@ -636,7 +878,8 @@ export class WindowManager extends GObject.Object { this.ext.settings.set_string("workspace-skip-tile", skippedArr.toString()); this.renderTree("workspace-toggle"); break; - case "LayoutStackedToggle": + } + case "LayoutStackedToggle": { if (!focusNodeWindow) return; if (!this.ext.settings.get_boolean("stacked-tiling-mode-enabled")) return; @@ -655,7 +898,7 @@ export class WindowManager extends GObject.Object { } focusNodeWindow.parentNode.layout = LAYOUT_TYPES.STACKED; let lastChild = focusNodeWindow.parentNode.lastChild; - if (lastChild.nodeType === NODE_TYPES.WINDOW) { + if (lastChild.nodeType === NODE_TYPES.WINDOW && this._isAlive(lastChild.nodeValue)) { lastChild.nodeValue.activate(global.display.get_current_time()); } } @@ -663,7 +906,8 @@ export class WindowManager extends GObject.Object { this.tree.attachNode = focusNodeWindow.parentNode; this.renderTree("layout-stacked-toggle"); break; - case "LayoutTabbedToggle": + } + case "LayoutTabbedToggle": { if (!focusNodeWindow) return; if (!this.ext.settings.get_boolean("tabbed-tiling-mode-enabled")) return; @@ -685,12 +929,13 @@ export class WindowManager extends GObject.Object { this.tree.attachNode = focusNodeWindow.parentNode; this.renderTree("layout-tabbed-toggle"); break; + } case "CancelOperation": - if (focusNodeWindow.mode === WINDOW_MODES.GRAB_TILE) { + if (focusNodeWindow && focusNodeWindow.mode === WINDOW_MODES.GRAB_TILE) { this.cancelGrab = true; } break; - case "PrefsOpen": + case "PrefsOpen": { let existWindow = Utils.findWindowWith(this.prefsTitle); if (existWindow && existWindow.get_workspace()) { existWindow @@ -701,6 +946,7 @@ export class WindowManager extends GObject.Object { this.ext.openPreferences(); } break; + } case "WindowSwapLastActive": if (focusNodeWindow) { let lastActiveWindow = global.display.get_tab_next( @@ -710,13 +956,15 @@ export class WindowManager extends GObject.Object { false ); let lastActiveNodeWindow = this.tree.findNode(lastActiveWindow); - this.tree.swapPairs(lastActiveNodeWindow, focusNodeWindow); - this.movePointerWith(focusNodeWindow); - this.renderTree("swap-last-active"); + if (lastActiveNodeWindow) { + this.tree.swapPairs(lastActiveNodeWindow, focusNodeWindow); + this.movePointerWith(focusNodeWindow); + this.renderTree("swap-last-active"); + } } break; case "SnapLayoutMove": - if (focusNodeWindow) { + if (focusNodeWindow && this._isAlive(focusNodeWindow.nodeValue)) { let workareaRect = focusNodeWindow.nodeValue.get_work_area_current_monitor(); let layoutAmount = action.amount; let layoutDirection = action.direction.toUpperCase(); @@ -738,7 +986,7 @@ export class WindowManager extends GObject.Object { layout.y = workareaRect.y; processGap = true; break; - case "CENTER": + case "CENTER": { let metaRect = this.focusMetaWindow.get_frame_rect(); layout.x = "center"; layout.y = "center"; @@ -749,6 +997,7 @@ export class WindowManager extends GObject.Object { height: metaRect.height, }; break; + } default: break; } @@ -766,10 +1015,11 @@ export class WindowManager extends GObject.Object { this.renderTree("snap-layout-move"); }, }); - break; } + // break outside the if: avoid falling through to ShowTabDecorationToggle + break; - case "ShowTabDecorationToggle": + case "ShowTabDecorationToggle": { if (!focusNodeWindow) return; if (!this.ext.settings.get_boolean("tabbed-tiling-mode-enabled")) return; @@ -780,6 +1030,7 @@ export class WindowManager extends GObject.Object { this.tree.attachNode = focusNodeWindow.parentNode; this.renderTree("showtab-decoration-enabled"); break; + } case "WindowResizeRight": this.resize(Meta.GrabOp.KEYBOARD_RESIZING_E, action.amount); @@ -804,6 +1055,7 @@ export class WindowManager extends GObject.Object { resize(grabOp, amount) { let metaWindow = this.focusMetaWindow; + if (!this._isAlive(metaWindow)) return; let display = global.display; this._handleGrabOpBegin(display, metaWindow, grabOp); @@ -849,12 +1101,14 @@ export class WindowManager extends GObject.Object { } enable() { + this.disabled = false; this._bindSignals(); this.reloadTree("enable"); Logger.debug(`extension:enable`); } findNodeWindow(metaWindow) { + if (!metaWindow) return null; return this.tree.findNode(metaWindow); } @@ -901,6 +1155,7 @@ export class WindowManager extends GObject.Object { getWindowsOnWorkspace(workspaceIndex) { const workspaceNode = this.tree.findNode(`ws${workspaceIndex}`); + if (!workspaceNode) return null; const workspaceWindows = workspaceNode.getNodeByType(NODE_TYPES.WINDOW); return workspaceWindows; } @@ -931,6 +1186,7 @@ export class WindowManager extends GObject.Object { } hideActorBorder(actor) { + if (!actor) return; if (actor.border) { actor.border.hide(); } @@ -945,7 +1201,7 @@ export class WindowManager extends GObject.Object { if (actor) { this.hideActorBorder(actor); } - if (nodeWindow.parentNode.isTabbed()) { + if (nodeWindow.parentNode && nodeWindow.parentNode.isTabbed()) { if (nodeWindow.tab) { // TODO: review the cleanup of the tab:St.Widget variable try { @@ -960,29 +1216,32 @@ export class WindowManager extends GObject.Object { // Window movement API move(metaWindow, rect) { - if (!metaWindow) return; + // Never touch an unmanaged window, or one that has not drawn its first + // frame yet (source of "meta_window_move_resize_internal: priv->is_ready") + if (!this._isAlive(metaWindow)) return; + if (!metaWindow._forgeReady) return; if (metaWindow.grabbed) return; - try { - // GNOME 49+ - metaWindow.set_unmaximize_flags(Meta.MaximizeFlags.BOTH); - metaWindow.unmaximize(); - } catch (e) { - // pre-49 fallback - metaWindow.unmaximize(Meta.MaximizeFlags.HORIZONTAL); - metaWindow.unmaximize(Meta.MaximizeFlags.VERTICAL); - metaWindow.unmaximize(Meta.MaximizeFlags.BOTH); + if (!rect) return; + + if (this._isMaximized(metaWindow)) { + this._unmaximize(metaWindow); } 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); + metaWindow.move_resize_frame( + true, + Math.round(rect.x), + Math.round(rect.y), + Math.round(rect.width), + Math.round(rect.height) + ); } moveCenter(metaWindow) { - if (!metaWindow) return; + if (!this._isAlive(metaWindow)) return; let frameRect = metaWindow.get_frame_rect(); const rectRequest = { x: "center", @@ -1003,6 +1262,7 @@ export class WindowManager extends GObject.Object { rectForMonitor(node, targetMonitor) { if (!node || (node && node.nodeType !== NODE_TYPES.WINDOW)) return null; if (targetMonitor < 0) return null; + if (!this._isAlive(node.nodeValue)) return null; let currentWorkArea = node.nodeValue.get_work_area_current_monitor(); let nextWorkArea = node.nodeValue.get_work_area_for_monitor(targetMonitor); @@ -1011,6 +1271,7 @@ export class WindowManager extends GObject.Object { if (!rect && node.mode === WINDOW_MODES.FLOAT) { rect = node.nodeValue.get_frame_rect(); } + if (!rect) return null; let hRatio = 1; let wRatio = 1; @@ -1068,6 +1329,11 @@ export class WindowManager extends GObject.Object { this._workspaceManagerSignals = undefined; } + if (this._settingsSignal) { + this.ext.settings.disconnect(this._settingsSignal); + this._settingsSignal = 0; + } + let numberOfWorkspaces = globalWsm.get_n_workspaces(); for (let i = 0; i < numberOfWorkspaces; i++) { @@ -1085,13 +1351,7 @@ export class WindowManager extends GObject.Object { 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; - } + this._disconnectWindowSignals(metaWindow); let windowActor = metaWindow.get_compositor_private(); if (windowActor && windowActor.actorSignals) { @@ -1102,21 +1362,12 @@ export class WindowManager extends GObject.Object { 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 (metaWindow._forgeReadyCleanup) { + metaWindow._forgeReadyCleanup(); + metaWindow._forgeReadyCleanup = null; } - if (windowActor && windowActor.splitBorder) { - windowActor.splitBorder.hide(); - if (global.window_group) { - global.window_group.remove_child(windowActor.splitBorder); - } - windowActor.splitBorder = undefined; - } + this._destroyActorDecorations(windowActor); } } @@ -1170,11 +1421,15 @@ export class WindowManager extends GObject.Object { } 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(); + try { + this.processFloats(); + this.tree.render(from); + this.updateDecorationLayout(); + this.updateBorderLayout(); + } catch (e) { + Logger.warn(`renderTree(${from}) failed: ${e.message}`); + } if (wasFrozen) this.freezeRender(); return false; }); @@ -1185,6 +1440,7 @@ export class WindowManager extends GObject.Object { processFloats() { this.allNodeWindows.forEach((nodeWindow) => { let metaWindow = nodeWindow.nodeValue; + if (!this._isAlive(metaWindow)) return; if (this.isFloatingExempt(metaWindow) || !this.isActiveWindowWorkspaceTiled(metaWindow)) { nodeWindow.float = true; } else { @@ -1207,10 +1463,8 @@ export class WindowManager extends GObject.Object { reloadTree(from) { if (!this._reloadTreeSrcId) { this._reloadTreeSrcId = GLib.idle_add(GLib.PRIORITY_LOW, () => { + this._reloadTreeSrcId = 0; 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; @@ -1218,7 +1472,6 @@ export class WindowManager extends GObject.Object { this.tree._initWorkspaces(); this.trackCurrentWindows(); this.renderTree(from); - this._reloadTreeSrcId = 0; return false; }); } @@ -1226,7 +1479,7 @@ export class WindowManager extends GObject.Object { sameParentMonitor(firstNode, secondNode) { if (!firstNode || !secondNode) return false; - if (!firstNode.nodeValue || !secondNode.nodeValue) return false; + if (!this._isAlive(firstNode.nodeValue) || !this._isAlive(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 @@ -1240,11 +1493,11 @@ export class WindowManager extends GObject.Object { showWindowBorders() { let metaWindow = this.focusMetaWindow; - if (!metaWindow) return; + if (!this._isAlive(metaWindow)) return; let windowActor = metaWindow.get_compositor_private(); if (!windowActor) return; let nodeWindow = this.findNodeWindow(metaWindow); - if (!nodeWindow) return; + if (!nodeWindow || !nodeWindow.parentNode) return; if (metaWindow.get_wm_class() === null) return; let borders = []; @@ -1252,15 +1505,7 @@ export class WindowManager extends GObject.Object { 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 = () => { - try { - // GNOME 49+ - return metaWindow.is_maximized() || metaWindow.is_fullscreen() || gap === 0; - } catch (e) { - // pre-49 fallback - return metaWindow.get_maximized() === 3 || metaWindow.is_fullscreen() || gap === 0; - } - }; + let maximized = () => this._isMaximized(metaWindow) || metaWindow.is_fullscreen() || gap === 0; let monitorCount = global.display.get_n_monitors(); let tiledChildren = this.tree.getTiledChildren(nodeWindow.parentNode.childNodes); let inset = 3; @@ -1311,18 +1556,7 @@ export class WindowManager extends GObject.Object { } } - if ( - gap === 0 || - (() => { - try { - // GNOME 49+ - return metaWindow.is_maximized(); - } catch (e) { - // pre-49 fallback - return metaWindow.get_maximized() === 1 || metaWindow.get_maximized() === 2; - } - })() - ) { + if (gap === 0 || this._isMaximized(metaWindow)) { inset = 0; } @@ -1333,7 +1567,7 @@ export class WindowManager extends GObject.Object { focusBorderEnabled && tilingModeEnabled && !nodeWindow.isFloat() && - !maximized && + !maximized() && // was "!maximized" (a function, always false) parentNode.childNodes.length === 1 && (parentNode.isCon() || parentNode.isMonitor()) && !(parentNode.isTabbed() || parentNode.isStacked()) @@ -1368,7 +1602,7 @@ export class WindowManager extends GObject.Object { // 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()); + global.window_group.insert_child_above(border, windowActor); } }); } @@ -1392,7 +1626,7 @@ export class WindowManager extends GObject.Object { if (parentNode) { let tiled = parentNode .getNodeByMode(WINDOW_MODES.TILE) - .filter((t) => t.isWindow() && !t.nodeValue.minimized); + .filter((t) => t.isWindow() && this._isAlive(t.nodeValue) && !t.nodeValue.minimized); if (tiled.length == 1 && hideGapWhenSingle) gap = 0; } } @@ -1407,12 +1641,27 @@ export class WindowManager extends GObject.Object { * */ trackWindow(_display, metaWindow) { + if (!metaWindow || metaWindow._forgeDead) return; + + // Make window types configurable + if (!this._validWindow(metaWindow)) return; + + let existNodeWindow = this.tree.findNode(metaWindow); + if (existNodeWindow) return; + + let windowActor = metaWindow.get_compositor_private(); + if (!windowActor) { + // Wayland: "window-created" arrives before the actor exists. + // Defer tracking until the actor is created instead of dropping the window. + this._deferTrackWindow(metaWindow); + return; + } + let autoSplit = this.ext.settings.get_boolean("auto-split-enabled"); - if (autoSplit && this.focusMetaWindow) { + if (autoSplit && this._isAlive(this.focusMetaWindow)) { let currentFocusNode = this.tree.findNode(this.focusMetaWindow); - if (currentFocusNode) { - let currentParentFocusNode = currentFocusNode.parentNode; - let layout = currentParentFocusNode.layout; + if (currentFocusNode && currentFocusNode.parentNode) { + let layout = currentFocusNode.parentNode.layout; if (layout === LAYOUT_TYPES.HSPLIT || layout === LAYOUT_TYPES.VSPLIT) { let frameRect = this.focusMetaWindow.get_frame_rect(); let splitHorizontal = frameRect.width > frameRect.height; @@ -1421,171 +1670,164 @@ export class WindowManager extends GObject.Object { } } } - // 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; + Logger.debug(`Meta Window ${metaWindow.get_title()} ${metaWindow.get_window_type()}`); - attachTarget = this.tree.attachNode; - attachTarget = attachTarget ? this.tree.findNode(attachTarget.nodeValue) : null; + let attachTarget; - 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; - } - } + const activeMonitor = global.display.get_current_monitor(); + const activeWorkspace = global.display.get_workspace_manager().get_active_workspace_index(); + let metaMonWs = `mo${activeMonitor}ws${activeWorkspace}`; - let nodeWindow = this.tree.createNode( - attachTarget.nodeValue, - NODE_TYPES.WINDOW, - metaWindow, - WINDOW_MODES.FLOAT - ); + // 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; + } - metaWindow.firstRender = true; + let windowNodes = metaMonWsNode.getNodeByType(NODE_TYPES.WINDOW); + let hasWindows = windowNodes.length > 0; - let windowActor = metaWindow.get_compositor_private(); + 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 + ); - 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("unmanaged", (_metaWindow) => { - this.hideActorBorder(windowActor); - }), - metaWindow.connect("focus", (_metaWindowFocus) => { + metaWindow.firstRender = true; + + // Only allow moving the window once it is ready + this._watchWindowReady(metaWindow, windowActor); + + if (!metaWindow.windowSignals) { + let windowSignals = [ + metaWindow.connect("position-changed", (_metaWindow) => { + if (_metaWindow._forgeDead) return; + this.updateMetaPositionSize(_metaWindow, "position-changed"); + }), + metaWindow.connect("size-changed", (_metaWindow) => { + if (_metaWindow._forgeDead) return; + this.updateMetaPositionSize(_metaWindow, "size-changed"); + }), + // Clean up on "unmanaging" (before the actor is destroyed) + metaWindow.connect("unmanaging", (_metaWindow) => { + this._onWindowUnmanaging(_metaWindow); + }), + metaWindow.connect("focus", (_metaWindowFocus) => { + if (_metaWindowFocus._forgeDead) return; + this.queueEvent({ + name: "focus-update", + callback: () => { + this.unfreezeRender(); + this.updateBorderLayout(); + this.updateDecorationLayout(); + let focusNodeWindow = this.tree.findNode(this.focusMetaWindow); + this.updateStackedFocus(focusNodeWindow); + this.updateTabbedFocus(focusNodeWindow); + 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: "focus-update", + name: "raise-float", callback: () => { - this.unfreezeRender(); - this.updateBorderLayout(); - this.updateDecorationLayout(); - this.updateStackedFocus(); - this.updateTabbedFocus(); - let focusNodeWindow = this.tree.findNode(this.focusMetaWindow); - this.movePointerWith(focusNodeWindow); + this.renderTree("raise-float-queue"); }, }); - 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; - } + } + this.tree.attachNode = focusNodeWindow; + } + this.renderTree("focus", true); + }), + metaWindow.connect("workspace-changed", (_metaWindow) => { + if (_metaWindow._forgeDead) return; + 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.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 (!windowActor.border) { + let border = new St.Bin({ style_class: "window-tiled-border" }); - if (global.window_group) global.window_group.add_child(border); + if (global.window_group) global.window_group.add_child(border); - windowActor.border = border; - border.show(); - } + windowActor.border = border; + border.show(); + } - this.postProcessWindow(nodeWindow); - this.queueEvent( - { - name: "window-create-queue", - callback: () => { - try { - // GNOME 49+ - metaWindow.set_unmaximize_flags(Meta.MaximizeFlags.BOTH); - metaWindow.unmaximize(); - } catch (e) { - // pre-49 fallback - metaWindow.unmaximize(Meta.MaximizeFlags.HORIZONTAL); - metaWindow.unmaximize(Meta.MaximizeFlags.VERTICAL); - metaWindow.unmaximize(Meta.MaximizeFlags.BOTH); - } - this.renderTree("window-create", true); - }, - }, - 200 - ); + this.postProcessWindow(nodeWindow); + this.queueEvent( + { + name: "window-create-queue", + callback: () => { + if (!this._isAlive(metaWindow)) return; + if (metaWindow._forgeReady && this._isMaximized(metaWindow)) { + this._unmaximize(metaWindow); + } + this.renderTree("window-create", true); + }, + }, + 200 + ); - let childNodes = this.tree.getTiledChildren(nodeWindow.parentNode.childNodes); - childNodes.forEach((n) => { - n.percent = 0.0; - }); - } - } + 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); - } + if (!this._isAlive(metaWindow)) return; + if (metaWindow.get_title() === this.prefsTitle) { + const ws = metaWindow.get_workspace(); + if (ws) ws.activate_with_focus(metaWindow, global.display.get_current_time()); + this.moveCenter(metaWindow); + } else { + // pass the node, not the MetaWindow + this.movePointerWith(nodeWindow); } } updateStackedFocus(focusNodeWindow) { - if (!focusNodeWindow) return; + if (!focusNodeWindow || !focusNodeWindow.parentNode) return; const parentNode = focusNodeWindow.parentNode; if (parentNode.layout === LAYOUT_TYPES.STACKED && !this._freezeRender) { parentNode.appendChild(focusNodeWindow); parentNode.childNodes - .filter((child) => child.isWindow()) + .filter((child) => child.isWindow() && this._isAlive(child.nodeValue)) .forEach((child) => child.nodeValue.raise()); this.queueEvent({ name: "render-focus-stack", @@ -1597,10 +1839,10 @@ export class WindowManager extends GObject.Object { } updateTabbedFocus(focusNodeWindow) { - if (!focusNodeWindow) return; + if (!focusNodeWindow || !focusNodeWindow.parentNode) return; if (focusNodeWindow.parentNode.layout === LAYOUT_TYPES.TABBED && !this._freezeRender) { const metaWindow = focusNodeWindow.nodeValue; - metaWindow.raise(); + if (this._isAlive(metaWindow)) metaWindow.raise(); } } @@ -1613,14 +1855,14 @@ export class WindowManager extends GObject.Object { let skipArr = skipWs.split(","); let skipThisWs = false; + let activeWorkspaceForWin = metaWindow.get_workspace(); + if (!activeWorkspaceForWin) return true; + let wsIndex = activeWorkspaceForWin.index(); + 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; - } + if (skipArr[i].trim() === `${wsIndex}`) { + skipThisWs = true; + break; } } return !skipThisWs; @@ -1650,6 +1892,8 @@ export class WindowManager extends GObject.Object { let windowsAll = this.windowsAllWorkspaces; for (let i = 0; i < windowsAll.length; i++) { let metaWindow = windowsAll[i]; + // Pre-existing windows have already drawn their first frame + metaWindow._forgeReady = true; this.trackWindow(global.display, metaWindow); // This updates and handles dynamic workspaces this.updateMetaWorkspaceMonitor( @@ -1662,6 +1906,7 @@ export class WindowManager extends GObject.Object { } _validWindow(metaWindow) { + if (!metaWindow) return false; let windowType = metaWindow.get_window_type(); return ( windowType === Meta.WindowType.NORMAL || @@ -1670,33 +1915,33 @@ export class WindowManager extends GObject.Object { ); } + /** + * Safety net: normally everything was already cleaned up on "unmanaging". + */ 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; - } - } + this._destroyActorDecorations(actor); - let splitBorder = actor.splitBorder; - if (splitBorder) { - if (global.window_group) { - global.window_group.remove_child(splitBorder); - splitBorder.hide(); - splitBorder = null; + try { + const mw = actor.get_meta_window ? actor.get_meta_window() : actor.meta_window; + if (mw && mw._forgeReadyCleanup) { + mw._forgeReadyCleanup(); + mw._forgeReadyCleanup = null; } + } catch (e) { + // the MetaWindow may already be gone } - let nodeWindow; - nodeWindow = this.tree.findNodeByActor(actor); + let nodeWindow = null; + try { + nodeWindow = this.tree.findNodeByActor(actor); + } catch (e) { + nodeWindow = null; + } - if (nodeWindow?.isWindow()) { + if (nodeWindow && nodeWindow.isWindow()) { + if (nodeWindow.nodeValue) nodeWindow.nodeValue._forgeDead = true; + if (this.lastFocusedWindow === nodeWindow) this.lastFocusedWindow = null; this.tree.removeNode(nodeWindow); - this.renderTree("window-destroy-quick", true); - this.removeFloatOverride(nodeWindow.nodeValue, true); } // find the next attachNode here @@ -1717,36 +1962,38 @@ export class WindowManager extends GObject.Object { * 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(); - } + if (!this._isAlive(metaWindow)) return; + if (!this._validWindow(metaWindow)) return; + 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)) { + // was "!this.grabOp === ..." (always false) + if (this.grabOp !== Meta.GrabOp.WINDOW_BASE) this.updateTabbedFocus(existNodeWindow); + this.updateStackedFocus(existNodeWindow); + } else { + if (this.floatingWindow(existNodeWindow)) { + existNodeWindow.nodeValue.raise(); } } } } - this.renderTree(from); } + this.renderTree(from); } /** @@ -1755,7 +2002,7 @@ export class WindowManager extends GObject.Object { */ updateMetaPositionSize(_metaWindow, from) { let focusMetaWindow = this.focusMetaWindow; - if (!focusMetaWindow) return; + if (!this._isAlive(focusMetaWindow)) return; let focusNodeWindow = this.findNodeWindow(focusMetaWindow); if (!focusNodeWindow) return; @@ -1769,17 +2016,7 @@ export class WindowManager extends GObject.Object { this._handleMoving(focusNodeWindow); } } else { - if ( - (() => { - try { - // GNOME 49+ - return !focusMetaWindow.is_maximized(); - } catch (e) { - // pre-49 fallback - return focusMetaWindow.get_maximized() === 0; - } - })() - ) { + if (!this._isMaximized(focusMetaWindow)) { this.renderTree(from); } } @@ -1801,7 +2038,9 @@ export class WindowManager extends GObject.Object { // Next, handle showing-desktop usually by Super + D if (!activeWsNode) return; - let allWindows = activeWsNode.getNodeByType(NODE_TYPES.WINDOW); + let allWindows = activeWsNode + .getNodeByType(NODE_TYPES.WINDOW) + .filter((w) => this._isAlive(w.nodeValue)); let allHiddenWindows = allWindows.filter((w) => { let metaWindow = w.nodeValue; return !metaWindow.showing_on_its_workspace() || metaWindow.minimized; @@ -1817,36 +2056,28 @@ export class WindowManager extends GObject.Object { let monWsNoMaxWindows = activeWsNode.getNodeByType(NODE_TYPES.MONITOR).filter((monitor) => { return ( monitor.getNodeByType(NODE_TYPES.WINDOW).filter((w) => { - return (() => { - try { - // GNOME 49+ - return w.nodeValue.is_maximized() || w.nodeValue.is_fullscreen(); - } catch (e) { - // pre-49 fallback - return ( - w.nodeValue.get_maximized() === Meta.MaximizeFlags.BOTH || - w.nodeValue.is_fullscreen() - ); - } - })(); + if (!this._isAlive(w.nodeValue)) return false; + return this._isMaximized(w.nodeValue) || w.nodeValue.is_fullscreen(); }).length === 0 ); }); + const focusMetaWindow = this.focusMetaWindow; + const focusActor = this._isAlive(focusMetaWindow) + ? focusMetaWindow.get_compositor_private() + : null; + const showTabs = this.ext.settings.get_boolean("showtab-decoration-enabled"); + 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) { + if (global.window_group.contains(con.decoration) && focusActor) { 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() - ); + global.window_group.insert_child_below(con.decoration, focusActor); } con.childNodes.forEach((cn) => { cn.render(); @@ -1877,6 +2108,7 @@ export class WindowManager extends GObject.Object { */ movePointerWith(nodeWindow, { force = false } = {}) { if (!nodeWindow || !nodeWindow._data) return; + if (!this._isAlive(nodeWindow.nodeValue)) return; const shouldWarp = force || this.ext.settings.get_boolean("move-pointer-focus-enabled"); if (shouldWarp) { this.storePointerLastPosition(this.lastFocusedWindow); @@ -1888,15 +2120,35 @@ export class WindowManager extends GObject.Object { this.tree.debugParentNodes(nodeWindow); } + _getDefaultSeat() { + // Newer GNOME: the Clutter backend comes from the stage context + try { + const backend = global.stage.context?.get_backend?.(); + if (backend) return backend.get_default_seat(); + } catch (e) {} + // Older versions + try { + if (typeof Clutter.get_default_backend === "function") { + return Clutter.get_default_backend().get_default_seat(); + } + } catch (e) {} + return null; + } + warpPointerToNodeWindow(nodeWindow) { + if (!nodeWindow || !this._isAlive(nodeWindow.nodeValue)) return; 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); + if (!newCoord) return; + try { + const seat = this._getDefaultSeat(); + if (seat && typeof seat.warp_pointer === "function") { + const x = Math.round(newCoord.x); + const y = Math.round(newCoord.y); + Logger.debug(`moved pointer to [${nodeWindow.nodeValue.get_title()}] at (${x},${y})`); + seat.warp_pointer(x, y); } + } catch (e) { + Logger.warn(`Failed to warp pointer: ${e.message}`); } } @@ -1930,9 +2182,10 @@ export class WindowManager extends GObject.Object { let nodeWinAtPointer = this.nodeWinAtPointer; - if (nodeWinAtPointer) { + if (nodeWinAtPointer && this._isAlive(nodeWinAtPointer.nodeValue)) { const targetRect = nodeWinAtPointer.nodeValue.get_frame_rect(); const parentNodeTarget = nodeWinAtPointer.parentNode; + if (!parentNodeTarget) return; const currPointer = this.getPointer(); const horizontal = parentNodeTarget.isHSplit() || parentNodeTarget.isTabbed(); const isMonParent = parentNodeTarget.nodeType === NODE_TYPES.MONITOR; @@ -1957,47 +2210,37 @@ export class WindowManager extends GObject.Object { } }; 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, + left: { + x: targetRect.x, + y: targetRect.y, + width: targetRect.width * regionWidth, + height: targetRect.height, + }, + right: { + x: targetRect.x + targetRect.width * (1 - regionWidth), + y: targetRect.y, + width: targetRect.width * regionWidth, + height: targetRect.height, + }, + top: { + x: targetRect.x, + y: targetRect.y, + width: targetRect.width, + height: targetRect.height * regionWidth, + }, + bottom: { + x: targetRect.x, + y: targetRect.y + targetRect.height * (1 - regionWidth), + width: targetRect.width, + height: targetRect.height * regionWidth, + }, + center: { + 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, + }, }; }; let referenceNode = null; @@ -2007,11 +2250,6 @@ export class WindowManager extends GObject.Object { className: "", targetRect: null, }; - let leftRegion; - let rightRegion; - let topRegion; - let bottomRegion; - let centerRegion; let previewWidth = 0.5; let hoverWidth = 0.3; @@ -2022,17 +2260,11 @@ export class WindowManager extends GObject.Object { // 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); + const isLeft = Utils.rectContainsPoint(hoverRegions.left, currPointer); + const isRight = Utils.rectContainsPoint(hoverRegions.right, currPointer); + const isTop = Utils.rectContainsPoint(hoverRegions.top, currPointer); + const isBottom = Utils.rectContainsPoint(hoverRegions.bottom, currPointer); + const isCenter = Utils.rectContainsPoint(hoverRegions.center, currPointer); if (isCenter) { if (centerLayout == "SWAP") { @@ -2100,17 +2332,12 @@ export class WindowManager extends GObject.Object { 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) { @@ -2127,9 +2354,6 @@ export class WindowManager extends GObject.Object { 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; @@ -2139,8 +2363,6 @@ export class WindowManager extends GObject.Object { }; } 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) { @@ -2157,9 +2379,6 @@ export class WindowManager extends GObject.Object { 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; @@ -2169,8 +2388,6 @@ export class WindowManager extends GObject.Object { }; } 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) { @@ -2203,6 +2420,13 @@ export class WindowManager extends GObject.Object { } if (!preview) { + // No valid drop target (e.g. stacked/tabbed monitor): do nothing + if (!containerNode && !(isCenter && centerLayout == "SWAP")) { + childNode.createCon = false; + childNode.detachWindow = false; + return; + } + const previousParent = focusNodeWindow.parentNode; this.tree.resetSiblingPercent(containerNode); this.tree.resetSiblingPercent(previousParent); @@ -2265,7 +2489,7 @@ export class WindowManager extends GObject.Object { } } } - previousParent.resetLayoutSingleChild(); + if (previousParent) previousParent.resetLayoutSingleChild(); } else { updatePreview(focusNodeWindow, previewParams); } @@ -2275,7 +2499,7 @@ export class WindowManager extends GObject.Object { } canMovePointerInsideNodeWindow(nodeWindow) { - if (nodeWindow && nodeWindow._data) { + if (nodeWindow && nodeWindow._data && this._isAlive(nodeWindow.nodeValue)) { const metaWindow = nodeWindow.nodeValue; const metaRect = metaWindow.get_frame_rect(); const pointerCoord = global.get_pointer(); @@ -2296,7 +2520,7 @@ export class WindowManager extends GObject.Object { pointerIsOverParentDecoration(nodeWindow, pointerCoord) { if (pointerCoord && nodeWindow && nodeWindow.parentNode) { let node = nodeWindow.parentNode; - if (node.isTabbed() || node.isStacked()) { + if ((node.isTabbed() || node.isStacked()) && node.rect) { return Utils.rectContainsPoint(node.rect, pointerCoord); } } @@ -2304,7 +2528,7 @@ export class WindowManager extends GObject.Object { } getPointerPositionInside(nodeWindow) { - if (nodeWindow && nodeWindow._data) { + if (nodeWindow && nodeWindow._data && this._isAlive(nodeWindow.nodeValue)) { const metaWindow = nodeWindow.nodeValue; const metaRect = metaWindow.get_frame_rect(); // on: last position of cursor inside window @@ -2323,7 +2547,7 @@ export class WindowManager extends GObject.Object { } storePointerLastPosition(nodeWindow) { - if (nodeWindow && nodeWindow._data) { + if (nodeWindow && nodeWindow._data && this._isAlive(nodeWindow.nodeValue)) { const metaWindow = nodeWindow.nodeValue; const metaRect = metaWindow.get_frame_rect(); const pointerCoord = global.get_pointer(); @@ -2339,10 +2563,9 @@ export class WindowManager extends GObject.Object { } findNodeWindowAtPointer(focusNodeWindow) { + if (!focusNodeWindow) return null; let pointerCoord = global.get_pointer(); - - let nodeWinAtPointer = this._findNodeWindowAtPointer(focusNodeWindow.nodeValue, pointerCoord); - return nodeWinAtPointer; + return this._findNodeWindowAtPointer(focusNodeWindow.nodeValue, pointerCoord); } /** @@ -2353,21 +2576,23 @@ export class WindowManager extends GObject.Object { _focusWindowUnderPointer() { // Break the loop if the user has disabled the feature // or if the window manager is disabled - if (!this.shouldFocusOnHover || this.disabled) return false; - - // We don't want to focus windows when the overview is visible - if (Main.overview.visible) return true; - - // Get the global mouse position - let pointer = global.get_pointer(); + if (!this.shouldFocusOnHover || this.disabled) { + this._pointerFocusTimeoutId = 0; + return false; + } - const metaWindow = this._getMetaWindowAtPointer(pointer); + // We don't want to focus windows when the overview is visible or during a grab + if (Main.overview.visible || this.grabOp) return true; - if (metaWindow) { - // If window is not null, focus it - metaWindow.focus(global.get_current_time()); - // Raise it to the top - metaWindow.raise(); + try { + const metaWindow = this._getMetaWindowAtPointer(global.get_pointer()); + // Only focus when the window changes (it used to refocus every 16ms) + if (metaWindow && metaWindow !== this.focusMetaWindow) { + metaWindow.focus(global.get_current_time()); + metaWindow.raise(); + } + } catch (e) { + Logger.warn(`focus-on-hover: ${e.message}`); } // Continue polling @@ -2377,23 +2602,28 @@ export class WindowManager extends GObject.Object { /** * Get the Meta.Window at the pointer coordinates * + * Skips actors of windows that are closing (still animating), minimized, + * hidden, or of unmanaged types (dock, desktop, ...). + * * @param {[number, number]} pointer x and y coordinates * @returns null if no window is found, otherwise the Meta.Window */ _getMetaWindowAtPointer(pointer) { - const windows = global.get_window_actors(); + const actors = global.get_window_actors(); const [x, y] = pointer; // Iterate through the windows in reverse order to get the top-most window - for (let i = windows.length - 1; i >= 0; i--) { - let window = windows[i]; - let metaWindow = window.meta_window; + for (let i = actors.length - 1; i >= 0; i--) { + const actor = actors[i]; + if (!actor || !actor.visible) continue; - let { x: wx, y: wy, width, height } = metaWindow.get_frame_rect(); + const mw = actor.get_meta_window ? actor.get_meta_window() : actor.meta_window; + if (!this._isAlive(mw) || !this._validWindow(mw)) continue; + if (mw.minimized || !mw.showing_on_its_workspace()) continue; - // Check if the position is within the window bounds + const { x: wx, y: wy, width, height } = mw.get_frame_rect(); if (x >= wx && x <= wx + width && y >= wy && y <= wy + height) { - return metaWindow; + return mw; } } @@ -2417,6 +2647,7 @@ export class WindowManager extends GObject.Object { for (let i = 0, n = sortedWindows.length; i < n; i++) { const w = sortedWindows[i]; + if (!this._isAlive(w)) continue; const metaRect = w.get_frame_rect(); const atPointer = Utils.rectContainsPoint(metaRect, pointer); if (atPointer) return this.tree.getNodeByValue(w); @@ -2430,7 +2661,7 @@ export class WindowManager extends GObject.Object { this.trackCurrentMonWs(); let focusMetaWindow = this.focusMetaWindow; - if (focusMetaWindow) { + if (this._isAlive(focusMetaWindow)) { let focusNodeWindow = this.findNodeWindow(focusMetaWindow); if (!focusNodeWindow) return; @@ -2466,7 +2697,7 @@ export class WindowManager extends GObject.Object { this._liveResizeSrcId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 16, () => { const metaWindow = focusNodeWindow.nodeValue; - if (!metaWindow || !focusNodeWindow.grabMode) { + if (!this._isAlive(metaWindow) || !focusNodeWindow.grabMode) { this._liveResizeSrcId = 0; return GLib.SOURCE_REMOVE; } @@ -2505,7 +2736,12 @@ export class WindowManager extends GObject.Object { this._stopLiveResizeLoop(); this.unfreezeRender(); let focusMetaWindow = this.focusMetaWindow; - if (!focusMetaWindow) return; + if (!this._isAlive(focusMetaWindow)) { + this.grabOp = null; + this.cancelGrab = false; + this.nodeWinAtPointer = null; + return; + } let focusNodeWindow = this.findNodeWindow(focusMetaWindow); if (focusNodeWindow && !this.cancelGrab) { @@ -2523,23 +2759,15 @@ export class WindowManager extends GObject.Object { } this._grabCleanup(focusNodeWindow); - if ( - (() => { - try { - // GNOME 49+ - return !focusMetaWindow.is_maximized(); - } catch (e) { - // pre-49 fallback - return focusMetaWindow.get_maximized() === 0; - } - })() - ) { + if (!this._isMaximized(focusMetaWindow)) { this.renderTree("grab-op-end"); } this.updateStackedFocus(focusNodeWindow); this.updateTabbedFocus(focusNodeWindow); this.nodeWinAtPointer = null; + // reset so focus-on-hover resumes after the grab + this.grabOp = null; } _grabCleanup(focusNodeWindow) { @@ -2551,7 +2779,8 @@ export class WindowManager extends GObject.Object { if (focusNodeWindow.previewHint) { focusNodeWindow.previewHint.hide(); - global.window_group.remove_child(focusNodeWindow.previewHint); + const parent = focusNodeWindow.previewHint.get_parent(); + if (parent) parent.remove_child(focusNodeWindow.previewHint); focusNodeWindow.previewHint.destroy(); focusNodeWindow.previewHint = null; } @@ -2567,6 +2796,7 @@ export class WindowManager extends GObject.Object { _handleResizing(focusNodeWindow) { if (!focusNodeWindow || focusNodeWindow.isFloat()) return; + if (!this._isAlive(this.focusMetaWindow)) return; let grabOps = Utils.decomposeGrabOp(this.grabOp); for (let grabOp of grabOps) { let initGrabOp = focusNodeWindow.initGrabOp; @@ -2638,6 +2868,7 @@ export class WindowManager extends GObject.Object { index = index - 1; } parentNodeForFocus = resizePairForWindow.parentNode.childNodes[index]; + if (!parentNodeForFocus || !firstWindowRect) return; firstRect = parentNodeForFocus.rect; secondRect = resizePairForWindow.rect; if (!firstRect || !secondRect) { @@ -2693,6 +2924,7 @@ export class WindowManager extends GObject.Object { index = index - 1; } parentNodeForFocus = resizePairForWindow.parentNode.childNodes[index]; + if (!parentNodeForFocus || !firstWindowRect) return; firstRect = parentNodeForFocus.rect; secondRect = resizePairForWindow.rect; if (!firstRect || !secondRect) { @@ -2730,6 +2962,8 @@ export class WindowManager extends GObject.Object { tiledWindows.forEach((nodeWin) => { if (nodeWin.nodeValue === draggingMetaWin) return; // GNOME owns this if (nodeWin.isFloat()) return; + // Skip windows being unmanaged or not ready yet (see _isAlive/_forgeReady) + if (!this._isAlive(nodeWin.nodeValue) || !nodeWin.nodeValue._forgeReady) return; if (!nodeWin.renderRect) return; const r = nodeWin.renderRect; if (r.width > 0 && r.height > 0) {