Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 176 additions & 129 deletions lib/extension/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -709,6 +702,7 @@ export class Tree extends Node {
global.window_group.remove_child(existingWsNode.actorBin);

this.removeChild(existingWsNode);
this._forgetRemoved(existingWsNode);
return true;
}

Expand Down Expand Up @@ -785,6 +779,35 @@ export class Tree extends Node {
/**
* Focuses on the next node, if metaWindow and tiled, raise it
*/
/**
* The window to focus when focus enters container `con` from outside it (from the `previous`
* side): a tabbed or stacked group's last-used window; otherwise, as before, a stack's last
* window, or the first or last tiled window.
*/
_entryWindow(con, previous) {
const isGroup = (n) => n.isTabbed() || n.isStacked();
let pick = isGroup(con) ? this._lastUsedIn(con) : null;
if (!pick) {
const tiled = con.getNodeByType(NODE_TYPES.WINDOW).filter((w) => w.isTile());
if (con.isStacked()) pick = tiled[tiled.length - 1] ?? con.lastChild;
else pick = previous ? tiled[tiled.length - 1] : tiled[0];
}
// A window inside a group further down (e.g. a split holding tabs): that group's last-used one
let outer = null;
for (let p = pick?.parentNode; p && p !== con; p = p.parentNode) if (isGroup(p)) outer = p;
return (outer && this._lastUsedIn(outer)) ?? pick;
}

/**
* The window last focused in a tabbed or stacked group, if it is still a visible tile anywhere
* inside it (null once it was closed, moved out, minimized or floated).
*/
_lastUsedIn(group) {
const last = group.lastTabFocus;
const node = last ? group.getNodeByValue(last) : null;
return node && node.isWindow() && node.isTile() && !last.minimized ? node : null;
}

focus(node, direction) {
if (!node) return null;
let next = this.next(node, direction);
Expand All @@ -799,20 +822,7 @@ export class Tree extends Node {
case NODE_TYPES.WINDOW:
break;
case NODE_TYPES.CON:
const tiledConWindows = next.getNodeByType(NODE_TYPES.WINDOW).filter((w) => w.isTile());
if (next.layout === LAYOUT_TYPES.STACKED) {
next = next.lastChild;
} else {
if (tiledConWindows.length > 1) {
if (previous) {
next = tiledConWindows[tiledConWindows.length - 1];
} else {
next = tiledConWindows[0];
}
} else {
next = tiledConWindows[0];
}
}
next = this._entryWindow(next, previous);
break;
case NODE_TYPES.MONITOR:
if (next.layout === LAYOUT_TYPES.STACKED) {
Expand All @@ -826,20 +836,7 @@ export class Tree extends Node {
}

if (next && next.nodeType === NODE_TYPES.CON) {
const tiledConWindows = next.getNodeByType(NODE_TYPES.WINDOW).filter((w) => w.isTile());
if (next.layout === LAYOUT_TYPES.STACKED) {
next = next.lastChild;
} else {
if (tiledConWindows.length > 1) {
if (previous) {
next = tiledConWindows[tiledConWindows.length - 1];
} else {
next = tiledConWindows[0];
}
} else {
next = tiledConWindows[0];
}
}
next = this._entryWindow(next, previous);
}
break;
}
Expand Down Expand Up @@ -1239,26 +1236,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) {
Expand All @@ -1271,6 +1266,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);
Expand Down Expand Up @@ -1504,100 +1520,131 @@ 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);
}

/**
* Process the child node here for the dimensions of the child tab/window,
* 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();
}
}

Expand Down
Loading