From 2337d348d070e00171210994fc1691b488b1833e Mon Sep 17 00:00:00 2001 From: mattchristenson Date: Mon, 28 Sep 2026 14:28:18 -0500 Subject: [PATCH] fix: tabbed and stacked groups go back to the split direction they had Toggling a split container to tabbed or stacked and back (Super+Shift+T / S) changed its direction: windows above each other came back side by side, and the other way round. Leaving a group always took a direction from the monitor's shape (determineSplitLayout()), not from the container. A container now remembers its split direction when it becomes a tabbed or stacked group (Node.setGroupLayout()), and every way back to a split restores it (Node.restoreSplitLayout()): both toggles, auto-exit of a tab group left with one window, a group left with one child, and turning the tabbed/stacked features off. The monitor's shape is still the fallback for a group that never was a split. Co-Authored-By: Claude Opus 5.5 --- lib/extension/tree.js | 34 ++++++++++++++++++++++++++++++++-- lib/extension/window.js | 32 +++++++++++++++++++++----------- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/lib/extension/tree.js b/lib/extension/tree.js index ea957d29..2ce29b91 100644 --- a/lib/extension/tree.js +++ b/lib/extension/tree.js @@ -587,10 +587,40 @@ export class Node extends GObject.Object { resetLayoutSingleChild() { let tabbedOrStacked = this.isTabbed() || this.isStacked(); if (tabbedOrStacked && this.singleOrNoChild()) { - this.layout = LAYOUT_TYPES.HSPLIT; + this.restoreSplitLayout(LAYOUT_TYPES.HSPLIT); } } + get layout() { + return this._layout; + } + + /** + * A split container that becomes a tabbed or stacked group remembers its split direction + * (`splitLayout`), wherever that happens, so that leaving the group can restore it. + */ + set layout(value) { + const split = this._layout === LAYOUT_TYPES.HSPLIT || this._layout === LAYOUT_TYPES.VSPLIT; + if (split && (value === LAYOUT_TYPES.TABBED || value === LAYOUT_TYPES.STACKED)) { + this.splitLayout = this._layout; + } + this._layout = value; + } + + /** + * Make this container a tabbed or stacked group; `remember = false` for a container just + * created for the group, whose split direction means nothing yet. + */ + setGroupLayout(layout, remember = true) { + this.layout = layout; + if (!remember) this.splitLayout = null; + } + + /** Leave a tabbed or stacked group: back to the split direction it had, else `fallback`. */ + restoreSplitLayout(fallback) { + this.layout = this.splitLayout ?? fallback; + } + singleOrNoChild() { return this.childNodes.length <= 1; } @@ -1256,7 +1286,7 @@ export class Tree extends Node { parentNode.layout === LAYOUT_TYPES.TABBED && parentNode.childNodes.length === 1 ) { - parentNode.layout = this.extWm.determineSplitLayout(); + parentNode.restoreSplitLayout(this.extWm.determineSplitLayout()); this.resetSiblingPercent(parentNode); parentNode.lastTabFocus = null; } diff --git a/lib/extension/window.js b/lib/extension/window.js index 61820e1e..8c260a3e 100644 --- a/lib/extension/window.js +++ b/lib/extension/window.js @@ -321,7 +321,7 @@ export class WindowManager extends GObject.Object { let stackedNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.STACKED); stackedNodes.forEach((node) => { node.prevLayout = node.layout; - node.layout = this.determineSplitLayout(); + node.restoreSplitLayout(this.determineSplitLayout()); }); } else { let hSplitNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.HSPLIT); @@ -329,7 +329,7 @@ export class WindowManager extends GObject.Object { Array.prototype.push.apply(hSplitNodes, vSplitNodes); hSplitNodes.forEach((node) => { if (node.prevLayout && node.prevLayout === LAYOUT_TYPES.STACKED) { - node.layout = LAYOUT_TYPES.STACKED; + node.setGroupLayout(LAYOUT_TYPES.STACKED); } }); } @@ -340,7 +340,7 @@ export class WindowManager extends GObject.Object { let tabbedNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.TABBED); tabbedNodes.forEach((node) => { node.prevLayout = node.layout; - node.layout = this.determineSplitLayout(); + node.restoreSplitLayout(this.determineSplitLayout()); }); } else { let hSplitNodes = this.tree.getNodeByLayout(LAYOUT_TYPES.HSPLIT); @@ -348,7 +348,7 @@ export class WindowManager extends GObject.Object { Array.prototype.push.apply(hSplitNodes, vSplitNodes); hSplitNodes.forEach((node) => { if (node.prevLayout && node.prevLayout === LAYOUT_TYPES.TABBED) { - node.layout = LAYOUT_TYPES.TABBED; + node.setGroupLayout(LAYOUT_TYPES.TABBED); } }); } @@ -640,20 +640,25 @@ export class WindowManager extends GObject.Object { if (!focusNodeWindow) return; if (!this.ext.settings.get_boolean("stacked-tiling-mode-enabled")) return; - if (focusNodeWindow.parentNode.isMonitor()) { + // A window directly on the workspace gets a container of its own first; that container's + // direction isn't the user's, so it isn't remembered + const parentBefore = focusNodeWindow.parentNode; + if (parentBefore.isMonitor()) { this.tree.split(focusNodeWindow, ORIENTATION_TYPES.HORIZONTAL, true); } + // (a floating window isn't wrapped: then its parent, the monitor, keeps its direction) + const wrapped = focusNodeWindow.parentNode !== parentBefore; currentLayout = focusNodeWindow.parentNode.layout; if (currentLayout === LAYOUT_TYPES.STACKED) { - focusNodeWindow.parentNode.layout = this.determineSplitLayout(); + focusNodeWindow.parentNode.restoreSplitLayout(this.determineSplitLayout()); this.tree.resetSiblingPercent(focusNodeWindow.parentNode); } else { if (currentLayout === LAYOUT_TYPES.TABBED) { focusNodeWindow.parentNode.lastTabFocus = null; } - focusNodeWindow.parentNode.layout = LAYOUT_TYPES.STACKED; + focusNodeWindow.parentNode.setGroupLayout(LAYOUT_TYPES.STACKED, !wrapped); let lastChild = focusNodeWindow.parentNode.lastChild; if (lastChild.nodeType === NODE_TYPES.WINDOW) { lastChild.nodeValue.activate(global.display.get_current_time()); @@ -667,18 +672,23 @@ export class WindowManager extends GObject.Object { if (!focusNodeWindow) return; if (!this.ext.settings.get_boolean("tabbed-tiling-mode-enabled")) return; - if (focusNodeWindow.parentNode.isMonitor()) { + // A window directly on the workspace gets a container of its own first; that container's + // direction isn't the user's, so it isn't remembered + const parentBeforeTabs = focusNodeWindow.parentNode; + if (parentBeforeTabs.isMonitor()) { this.tree.split(focusNodeWindow, ORIENTATION_TYPES.HORIZONTAL, true); } + // (a floating window isn't wrapped: then its parent, the monitor, keeps its direction) + const wrappedForTabs = focusNodeWindow.parentNode !== parentBeforeTabs; currentLayout = focusNodeWindow.parentNode.layout; if (currentLayout === LAYOUT_TYPES.TABBED) { - focusNodeWindow.parentNode.layout = this.determineSplitLayout(); + focusNodeWindow.parentNode.restoreSplitLayout(this.determineSplitLayout()); this.tree.resetSiblingPercent(focusNodeWindow.parentNode); focusNodeWindow.parentNode.lastTabFocus = null; } else { - focusNodeWindow.parentNode.layout = LAYOUT_TYPES.TABBED; + focusNodeWindow.parentNode.setGroupLayout(LAYOUT_TYPES.TABBED, !wrappedForTabs); focusNodeWindow.parentNode.lastTabFocus = focusNodeWindow.nodeValue; } this.unfreezeRender(); @@ -2242,7 +2252,7 @@ export class WindowManager extends GObject.Object { } else if (isTop || isBottom) { childNode.layout = LAYOUT_TYPES.VSPLIT; } else if (isCenter) { - childNode.layout = LAYOUT_TYPES[centerLayout]; + childNode.setGroupLayout(LAYOUT_TYPES[centerLayout], false); // a new container } } else if (childNode.detachWindow) { const orientation =