]> Freerunner's - simpledwindle.git/commitdiff
tile all windows by default, set keepBelow-flag for tiled ones
authorAndre Ramnitz <andre.ramnitz@mail.de>
Mon, 19 Oct 2026 16:55:42 +0000 (18:55 +0200)
committerAndre Ramnitz <andre.ramnitz@mail.de>
Mon, 19 Oct 2026 16:55:42 +0000 (18:55 +0200)
contents/code/main.js
metadata.json

index 33f3a4dc8d43ab32c3bdec67d1aa9cd939ea6991..a0b71c349958ff790e3d75da59a99d6d2140348e 100644 (file)
 
 "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");
 
index 8cf7a7dd782686c8e438dae6d8ce3fc8b6f9fc7d..6a2acd3f43fd0a78a7a496e85b678399067e2e6d 100644 (file)
@@ -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",