diff --git a/lib/extension/tree.js b/lib/extension/tree.js index ea957d29..f4073731 100644 --- a/lib/extension/tree.js +++ b/lib/extension/tree.js @@ -351,13 +351,6 @@ export class Node extends GObject.Object { } removeChild(node) { - if (node.isTabbed() && node.decoration) { - node.decoration.hide(); - node.decoration.destroy_all_children(); - node.decoration.destroy(); - node.decoration = null; - } - let refNode; if (this.contains(node)) { // Since contains() tries to find node on all descendants, @@ -709,6 +702,7 @@ export class Tree extends Node { global.window_group.remove_child(existingWsNode.actorBin); this.removeChild(existingWsNode); + this._forgetRemoved(existingWsNode); return true; } @@ -1239,26 +1233,24 @@ export class Tree extends Node { let parentNode = node.parentNode; // If parent has only this window, remove the parent instead - if (parentNode.childNodes.length === 1 && parentNode.nodeType !== NODE_TYPES.MONITOR) { - let existParent = parentNode.parentNode; - oldChild = existParent.removeChild(parentNode); - cleanUpParent(existParent); - } else { - let existParent = node.parentNode; - oldChild = existParent.removeChild(node); - if (!this.extWm.floatingWindow(node)) cleanUpParent(existParent); - } + const removeParent = + parentNode.childNodes.length === 1 && parentNode.nodeType !== NODE_TYPES.MONITOR; + const removed = removeParent ? parentNode : node; + const existParent = removed.parentNode; + oldChild = existParent.removeChild(removed); + if (removeParent || !this.extWm.floatingWindow(node)) cleanUpParent(existParent); + this._forgetRemoved(removed); // If only a single tab remains, exit tabbed layout if ( this.settings.get_boolean("auto-exit-tabbed") && - parentNode.nodeType === NODE_TYPES.CON && - parentNode.layout === LAYOUT_TYPES.TABBED && - parentNode.childNodes.length === 1 + existParent.nodeType === NODE_TYPES.CON && + existParent.layout === LAYOUT_TYPES.TABBED && + existParent.childNodes.length === 1 ) { - parentNode.layout = this.extWm.determineSplitLayout(); - this.resetSiblingPercent(parentNode); - parentNode.lastTabFocus = null; + existParent.layout = this.extWm.determineSplitLayout(); + this.resetSiblingPercent(existParent); + existParent.lastTabFocus = null; } if (node === this.attachNode) { @@ -1271,6 +1263,27 @@ export class Tree extends Node { return oldChild ? true : false; } + /** + * A subtree has left the tree for good: destroy the tabs of its windows and the tab bars of its + * containers (whatever their layout: a container can still show tabs after it stopped being + * tabbed, e.g. when all of its windows close at once). Tab bars of other containers keep their + * tabs; a tab bar being destroyed only detaches tabs that belong to windows still in the tree. + */ + _forgetRemoved(root) { + const windows = root.isWindow() ? [root] : root.getNodeByType(NODE_TYPES.WINDOW); + windows.forEach((w) => { + w.tab?.destroy(); + w.tab = null; + }); + const cons = new Set([...(root.isCon() ? [root] : []), ...root.getNodeByType(NODE_TYPES.CON)]); + cons.forEach((con) => { + if (!con.decoration) return; + con.decoration.remove_all_children(); + con.decoration.destroy(); + con.decoration = null; + }); + } + render(from) { Logger.debug(`render tree ${from ? "from " + from : ""}`); this.processNode(this); @@ -1504,26 +1517,43 @@ export class Tree extends Node { * */ processStacked(node, child, params, index) { - let layout = node.layout; - let nodeWidth = node.rect.width; - let nodeHeight = node.rect.height; - let nodeX = node.rect.x; - let nodeY = node.rect.y; - let stackHeight = this.defaultStackHeight; - - if (layout === LAYOUT_TYPES.STACKED) { - if (node.childNodes.length > 1) { - nodeY += stackHeight * index; - nodeHeight -= stackHeight * index; - } - + if (node.layout !== LAYOUT_TYPES.STACKED) return; + const rect = node.rect; + const rows = params.tiledChildren.length; + if (!this.hasTitleList(node)) { + // The cascade as before, where the top of each window before the focused one shows + const stackHeight = node.childNodes.length > 1 ? this.defaultStackHeight : 0; child.rect = { - x: nodeX, - y: nodeY, - width: nodeWidth, - height: nodeHeight, + x: rect.x, + y: rect.y + stackHeight * index, + width: rect.width, + height: rect.height - stackHeight * index, }; + return; } + // A list of every window's title at the top (like i3), and all the windows in the same place + // below it: the focused one is raised, and any other is one click (or Super+J/K) away + const listHeight = params.stackedHeight * rows; + this._placeDecoration(node, child, index, listHeight, params.stackedHeight); + child.rect = { + x: rect.x, + y: rect.y + listHeight, + width: rect.width, + height: rect.height - listHeight, + }; + } + + /** + * Whether a stacked container shows a title list (else the old cascade): title bars are on + * (showtab-decoration-enabled), it has a bar to draw them in (a workspace-level stack has none), + * and every tiled child is a window with a title row (a container, or a window whose app is + * unknown, has none). + */ + hasTitleList(node) { + if (!node.isStacked() || !node.decoration) return false; + if (!this.settings.get_boolean("showtab-decoration-enabled")) return false; + const tiled = this.getTiledChildren(node.childNodes); + return tiled.length > 0 && tiled.every((c) => c.isWindow() && c.isNodeValid() && c.tab); } /** @@ -1531,73 +1561,87 @@ export class Tree extends Node { * It will be moved to the Node class in the future as Node.render() * */ - processTabbed(node, child, params, _index) { - let layout = node.layout; - let nodeRect = node.rect; - let nodeWidth; - let nodeHeight; - let nodeX; - let nodeY; - - if (layout === LAYOUT_TYPES.TABBED) { - nodeWidth = nodeRect.width; - nodeX = nodeRect.x; - nodeY = nodeRect.y; - nodeHeight = nodeRect.height; - - let alwaysShowDecorationTab = true; - - if (node.childNodes.length > 1 || alwaysShowDecorationTab) { - nodeY = nodeRect.y + params.stackedHeight; - nodeHeight = nodeRect.height - params.stackedHeight; - if (node.decoration && child.isWindow() && child.isNodeValid()) { - let gap = this.extWm.calculateGaps(node); - let renderRect = this.processGap(node); - // Border actor may be gone if the window was destroyed mid-render - let borderWidth = 0; - try { - if (child.actor?.border) { - borderWidth = child.actor.border.get_theme_node().get_border_width(St.Side.TOP); - } - } catch (e) {} - - // Make adjustments to the gaps - let adjust = 4 * Utils.dpi(); - let adjustWidth = renderRect.width + (borderWidth * 2 + gap) / adjust; - let adjustX = renderRect.x - (gap + borderWidth * 2) / (adjust * 2); - let adjustY = renderRect.y - adjust; + processTabbed(node, child, params, index) { + if (node.layout !== LAYOUT_TYPES.TABBED) return; + const rect = node.rect; + this._placeDecoration(node, child, index, params.stackedHeight); + child.rect = { + x: rect.x, + y: rect.y + params.stackedHeight, + width: rect.width, + height: rect.height - params.stackedHeight, + }; + } - if (gap === 0) { - adjustY = renderRect.y; - } + /** + * Put a tabbed or stacked container's title bar at its top, `height` px high, and `child`'s tab + * in it: side by side for tabs, or, with `rowHeight`, one row of that height per window (a stack). + * The bar itself is placed once per container, with its first child (`index` 0). + */ + _placeDecoration(node, child, index, height, rowHeight = 0) { + const vertical = rowHeight > 0; + const decoration = node.decoration; + if (!decoration) return; + if (index === 0) this._placeBar(node, child, decoration, height, vertical); + if (!child.isWindow() || !child.isNodeValid()) return; + if (child.tab && !decoration.contains(child.tab)) { + try { + decoration.add_child(child.tab); + } catch (e) {} + } + // A stack's rows get their share of the list exactly, less their CSS margins, so that they + // neither overflow it nor take their natural height + let rowSize = -1; + if (vertical && child.tab) { + rowSize = rowHeight; + try { + const theme = child.tab.get_theme_node(); + rowSize -= theme.get_margin(St.Side.TOP) + theme.get_margin(St.Side.BOTTOM); + } catch (e) { + rowSize -= 2; // not styled yet: the default theme's margins + } + } + child.tab?.set_height(Math.max(rowSize, -1)); + child.render(); + } - let decoration = node.decoration; + /** The container-level part of _placeDecoration(): the bar's place, size and direction. */ + _placeBar(node, child, decoration, height, vertical) { + let gap = this.extWm.calculateGaps(node); + let renderRect = this.processGap(node); + // The windows' border, from a window of the group (the first child may be a container). + // Border actor may be gone if the window was destroyed mid-render + const window = child.isWindow() ? child : node.childNodes.find((c) => c.isWindow()); + let borderWidth = 0; + try { + if (window?.actor?.border) { + borderWidth = window.actor.border.get_theme_node().get_border_width(St.Side.TOP); + } + } catch (e) {} - if (decoration !== null && decoration !== undefined) { - decoration.set_size(adjustWidth, params.stackedHeight); - decoration.set_position(adjustX, adjustY); - if (params.tiledChildren.length > 0 && params.stackedHeight !== 0) { - decoration.show(); - } else { - decoration.hide(); - } - if (child.tab && !decoration.contains(child.tab)) { - try { - decoration.add_child(child.tab); - } catch (e) {} - } - } + // Make adjustments to the gaps + let adjust = 4 * Utils.dpi(); + let adjustWidth = renderRect.width + (borderWidth * 2 + gap) / adjust; + let adjustX = renderRect.x - (gap + borderWidth * 2) / (adjust * 2); + let adjustY = renderRect.y - adjust; - child.render(); - } - } + if (gap === 0) { + adjustY = renderRect.y; + } - child.rect = { - x: nodeX, - y: nodeY, - width: nodeWidth, - height: nodeHeight, - }; + if ("orientation" in decoration) { + decoration.orientation = vertical + ? Clutter.Orientation.VERTICAL + : Clutter.Orientation.HORIZONTAL; + } else { + decoration.vertical = vertical; // GNOME < 48 + } + decoration.set_size(adjustWidth, height); + decoration.set_position(adjustX, adjustY); + if (height !== 0) { + decoration.show(); + } else { + decoration.hide(); } } diff --git a/lib/extension/window.js b/lib/extension/window.js index 61820e1e..9b1a1d04 100644 --- a/lib/extension/window.js +++ b/lib/extension/window.js @@ -518,7 +518,11 @@ export class WindowManager extends GObject.Object { if (this.eventQueue.length <= 0) { this.unfreezeRender(); if (focusNodeWindow.parentNode.layout === LAYOUT_TYPES.STACKED) { - focusNodeWindow.parentNode.appendChild(focusNodeWindow); + // With a title list the stack keeps the order the move gave it; the old cascade + // shows the focused window last + if (!this.tree.hasTitleList(focusNodeWindow.parentNode)) { + focusNodeWindow.parentNode.appendChild(focusNodeWindow); + } focusNodeWindow.nodeValue.raise(); focusNodeWindow.nodeValue.activate(global.display.get_current_time()); this.renderTree("move-stacked-queue"); @@ -945,7 +949,7 @@ export class WindowManager extends GObject.Object { if (actor) { this.hideActorBorder(actor); } - if (nodeWindow.parentNode.isTabbed()) { + if (nodeWindow.parentNode.isTabbed() || nodeWindow.parentNode.isStacked()) { if (nodeWindow.tab) { // TODO: review the cleanup of the tab:St.Widget variable try { @@ -1269,7 +1273,7 @@ export class WindowManager extends GObject.Object { const floatingWindow = nodeWindow.isFloat(); const tiledBorder = windowActor.border; - if (parentNode.isTabbed()) { + if (parentNode.isTabbed() || parentNode.isStacked()) { if (nodeWindow.tab) { nodeWindow.tab.add_style_class_name("window-tabbed-tab-active"); } @@ -1492,9 +1496,9 @@ export class WindowManager extends GObject.Object { this.unfreezeRender(); this.updateBorderLayout(); this.updateDecorationLayout(); - this.updateStackedFocus(); - this.updateTabbedFocus(); - let focusNodeWindow = this.tree.findNode(this.focusMetaWindow); + const focusNodeWindow = this.tree.findNode(this.focusMetaWindow); + this.updateStackedFocus(focusNodeWindow, true); + this.updateTabbedFocus(focusNodeWindow); this.movePointerWith(focusNodeWindow); }, }); @@ -1579,10 +1583,20 @@ export class WindowManager extends GObject.Object { } } - updateStackedFocus(focusNodeWindow) { + /** + * `fromFocusChange`: called for every focus change (not only moves, swaps and the overview). + */ + updateStackedFocus(focusNodeWindow, fromFocusChange = false) { if (!focusNodeWindow) return; const parentNode = focusNodeWindow.parentNode; - if (parentNode.layout === LAYOUT_TYPES.STACKED && !this._freezeRender) { + if (parentNode.layout !== LAYOUT_TYPES.STACKED || this._freezeRender) return; + if (this.tree.hasTitleList(parentNode)) { + // The stack keeps its order (its title list shows every window): only bring the focused + // window to the front + focusNodeWindow.nodeValue.raise(); + } else if (!fromFocusChange) { + // No title list (title bars off, or a mixed stack): the cascade as before, the focused + // window moved to the end so that the tops of the others show above it parentNode.appendChild(focusNodeWindow); parentNode.childNodes .filter((child) => child.isWindow()) @@ -1838,7 +1852,9 @@ export class WindowManager extends GObject.Object { 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) { + const grouped = con.isTabbed() || con.isStacked(); + const hasRows = con.decoration?.get_n_children() > 0; // not for a group of containers only + if (con.decoration && grouped && tiled.length > 0 && showTabs && hasRows) { con.decoration.show(); if (global.window_group.contains(con.decoration) && this.focusMetaWindow) { global.window_group.remove_child(con.decoration);