From e98d23d0715cd5f2787a0afb05a21513d7823da9 Mon Sep 17 00:00:00 2001 From: Andre Ramnitz Date: Mon, 19 Oct 2026 18:55:42 +0200 Subject: [PATCH] tile all windows by default, set keepBelow-flag for tiled ones --- contents/code/main.js | 309 +++++++++++++++++++++++++++++++++++++----- metadata.json | 2 +- 2 files changed, 276 insertions(+), 35 deletions(-) diff --git a/contents/code/main.js b/contents/code/main.js index 33f3a4d..a0b71c3 100644 --- a/contents/code/main.js +++ b/contents/code/main.js @@ -1,70 +1,312 @@ "use strict"; -// SimpleDwindle 0.1 -// Proof of Concept: Tiling / Floating Z-order +// SimpleDwindle 0.2 +// Minimal chronological Dwindle layout for KWin 6 -const tiledWindows = []; +const GAP = 8; -function log(message) { - print("[SimpleDwindle] " + message); +const layouts = []; +const floating = []; +const managed = []; + +function log(msg) { + print("[SimpleDwindle] " + msg); +} + +function eligible(w) { + return w && w.normalWindow && + w.moveable && w.resizeable && + !w.fullScreen && !w.minimized && + !w.transient; +} + +function contains(list, w) { + return list.indexOf(w) !== -1; +} + +function layoutFor(w) { + const desktop = w.desktops.length + ? w.desktops[0] + : workspace.currentDesktop; + + let layout = layouts.find(l => + l.output === w.output && + l.desktop === desktop + ); + + if (!layout) { + layout = { + output: w.output, + desktop: desktop, + root: null, + last: null + }; + layouts.push(layout); + } + + return layout; +} + +function leaf(w, parent = null) { + return { + window: w, + parent: parent, + first: null, + second: null, + vertical: false + }; +} + +function findLeaf(node, w) { + if (!node) + return null; + + if (node.window === w) + return node; + + return findLeaf(node.first, w) || + findLeaf(node.second, w); +} + +function rightmost(node) { + if (!node) + return null; + + while (node.second) + node = node.second; + + return node; +} + +function depth(node) { + let d = 0; + + while (node && node.parent) { + d++; + node = node.parent; + } + + return d; +} + +function insertWindow(layout, w) { + if (!layout.root) { + layout.root = leaf(w); + layout.last = w; + return; + } + + let target = findLeaf(layout.root, layout.last); + + if (!target) + target = rightmost(layout.root); + + const d = depth(target); + const oldWindow = target.window; + + target.window = null; + target.vertical = (d % 2 === 0); + + target.first = leaf(oldWindow, target); + target.second = leaf(w, target); + + layout.last = w; +} + +function removeNode(layout, w) { + const node = findLeaf(layout.root, w); + + if (!node) + return false; + + if (!node.parent) { + layout.root = null; + layout.last = null; + return true; + } + + const parent = node.parent; + const sibling = parent.first === node + ? parent.second + : parent.first; + + const grandparent = parent.parent; + + sibling.parent = grandparent; + + if (!grandparent) { + layout.root = sibling; + } else if (grandparent.first === parent) { + grandparent.first = sibling; + } else { + grandparent.second = sibling; + } + + if (layout.last === w) { + const lastNode = rightmost(layout.root); + layout.last = lastNode ? lastNode.window : null; + } + + return true; } -function isCandidate(window) { - return window && window.normalWindow && - !window.fullScreen; +function applyNode(node, rect) { + if (!node) + return; + + if (node.window) { + const w = node.window; + + const x = Math.round(rect.x + GAP / 2); + const y = Math.round(rect.y + GAP / 2); + const width = Math.max( + 1, Math.round(rect.width - GAP) + ); + const height = Math.max( + 1, Math.round(rect.height - GAP) + ); + + w.keepAbove = false; + w.keepBelow = true; + + w.frameGeometry = { + x: x, + y: y, + width: width, + height: height + }; + + return; + } + + if (node.vertical) { + const half = rect.width / 2; + + applyNode(node.first, { + x: rect.x, + y: rect.y, + width: half, + height: rect.height + }); + + applyNode(node.second, { + x: rect.x + half, + y: rect.y, + width: rect.width - half, + height: rect.height + }); + } else { + const half = rect.height / 2; + + applyNode(node.first, { + x: rect.x, + y: rect.y, + width: rect.width, + height: half + }); + + applyNode(node.second, { + x: rect.x, + y: rect.y + half, + width: rect.width, + height: rect.height - half + }); + } } -function isTiled(window) { - return tiledWindows.indexOf(window) !== -1; +function arrange(layout) { + if (!layout.root) + return; + + const area = workspace.clientArea( + KWin.MaximizeArea, + layout.output, + layout.desktop + ); + + applyNode(layout.root, area); } -function setTiled(window) { - if (!isCandidate(window) || isTiled(window)) +function tileWindow(w) { + if (!eligible(w) || contains(managed, w)) return; - tiledWindows.push(window); + const fi = floating.indexOf(w); + if (fi !== -1) + floating.splice(fi, 1); - window.keepAbove = false; - window.keepBelow = true; + managed.push(w); - log("TILED: " + window.caption); + const layout = layoutFor(w); + + insertWindow(layout, w); + arrange(layout); + + log("TILED: " + w.caption); } -function setFloating(window) { - const index = tiledWindows.indexOf(window); +function detachWindow(w) { + const index = managed.indexOf(w); if (index === -1) + return false; + + managed.splice(index, 1); + + for (const layout of layouts) { + if (removeNode(layout, w)) { + arrange(layout); + break; + } + } + + return true; +} + +function floatWindow(w) { + if (!detachWindow(w)) return; - tiledWindows.splice(index, 1); + if (!contains(floating, w)) + floating.push(w); - window.keepBelow = false; + w.keepBelow = false; - log("FLOATING: " + window.caption); + log("FLOATING: " + w.caption); } function toggleFloating() { - const window = workspace.activeWindow; + const w = workspace.activeWindow; - if (!window) + if (!w || !w.normalWindow) return; - if (isTiled(window)) - setFloating(window); - else - setTiled(window); + if (contains(managed, w)) { + floatWindow(w); + } else { + tileWindow(w); + } } -// Remove closed windows from our list -workspace.windowRemoved.connect(function(window) { - const index = tiledWindows.indexOf(window); +workspace.windowAdded.connect(function(w) { + if (eligible(w)) + tileWindow(w); +}); + +workspace.windowRemoved.connect(function(w) { + detachWindow(w); + const index = floating.indexOf(w); if (index !== -1) - tiledWindows.splice(index, 1); + floating.splice(index, 1); +}); + +workspace.screensChanged.connect(function() { + for (const layout of layouts) + arrange(layout); }); -// Keyboard shortcut registerShortcut( "SimpleDwindleToggleFloating", "SimpleDwindle: Toggle Floating", @@ -72,6 +314,5 @@ registerShortcut( toggleFloating ); -log("Version 0.1 initialized"); - +log("Version 0.2 initialized"); diff --git a/metadata.json b/metadata.json index 8cf7a7d..6a2acd3 100644 --- a/metadata.json +++ b/metadata.json @@ -3,7 +3,7 @@ "Id": "simpledwindle", "Name": "SimpleDwindle", "Description": "Minimal tiling and floating window manager", - "Version": "0.1", + "Version": "0.2", "License": "MIT" }, "X-Plasma-API": "javascript", -- 2.55.0