-
"use strict";
-// SimpleDwindle 0.2
-// Minimal chronological Dwindle layout for KWin 6
+// SimpleDwindle
+const VERSION = "0.3.2";
+// KWin 6.8 - Minimal chronological Dwindle
+
+// ------------------------------------------------------------
+// Configuration
+// ------------------------------------------------------------
const GAP = 8;
+// Always floating, never tiled
+const FLOATING_CLASSES = [
+ // "org.kde.dolphin",
+ "org.kde.kcalc",
+ "systemsettings",
+];
+
+// Completely ignored by SimpleDwindle
+const IGNORED_CLASSES = [
+ "wl-copy",
+];
+
+// ------------------------------------------------------------
+// State
+// ------------------------------------------------------------
+
const layouts = [];
-const floating = [];
const managed = [];
+const floating = [];
+const suspended = [];
+const watched = [];
+
+// ------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------
function log(msg) {
- print("[SimpleDwindle] " + msg);
+ console.log("[SimpleDwindle] " + msg);
+}
+
+function contains(list, w) {
+ return list.indexOf(w) !== -1;
+}
+
+function removeFromList(list, w) {
+ const index = list.indexOf(w);
+
+ if (index !== -1)
+ list.splice(index, 1);
+}
+
+function windowClass(w) {
+ return String(w.resourceClass || "").toLowerCase();
+}
+
+function matchesClass(w, classes) {
+ return classes.some(c =>
+ c.toLowerCase() === windowClass(w)
+ );
}
-function eligible(w) {
- return w && w.normalWindow &&
- w.moveable && w.resizeable &&
- !w.fullScreen && !w.minimized &&
+function isCandidate(w) {
+ return w &&
+ w.normalWindow &&
+ w.moveable &&
+ w.resizeable &&
!w.transient;
}
-function contains(list, w) {
- return list.indexOf(w) !== -1;
+function isIgnored(w) {
+ return matchesClass(w, IGNORED_CLASSES);
+}
+
+function isForcedFloating(w) {
+ return matchesClass(w, FLOATING_CLASSES);
}
+// ------------------------------------------------------------
+// Layout management
+// ------------------------------------------------------------
+
function layoutFor(w) {
const desktop = w.desktops.length
? w.desktops[0]
root: null,
last: null
};
+
layouts.push(layout);
}
return layout;
}
+function findWindowLayout(w) {
+ return layouts.find(l =>
+ findLeaf(l.root, w) !== null
+ );
+}
+
+// ------------------------------------------------------------
+// Binary tree
+// ------------------------------------------------------------
+
function leaf(w, parent = null) {
return {
window: w,
}
const parent = node.parent;
+
const sibling = parent.first === node
? parent.second
: parent.first;
if (layout.last === w) {
const lastNode = rightmost(layout.root);
- layout.last = lastNode ? lastNode.window : null;
+ layout.last = lastNode
+ ? lastNode.window
+ : null;
}
return true;
}
+// ------------------------------------------------------------
+// Geometry
+// ------------------------------------------------------------
+
function applyNode(node, rect) {
if (!node)
return;
if (node.window) {
const w = node.window;
+ // Fullscreen windows retain their tree position,
+ // but KWin controls their geometry.
+ if (w.fullScreen || w.minimized)
+ return;
+
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)
);
}
function arrange(layout) {
- if (!layout.root)
+ if (!layout || !layout.root)
return;
const area = workspace.clientArea(
applyNode(layout.root, area);
}
+// ------------------------------------------------------------
+// Window states
+// ------------------------------------------------------------
+
function tileWindow(w) {
- if (!eligible(w) || contains(managed, w))
+ log("TILED: " + w.caption + " [" + w.resourceClass + "]");
+
+ if (!isCandidate(w) ||
+ isIgnored(w) ||
+ contains(managed, w) ||
+ w.minimized)
return;
- const fi = floating.indexOf(w);
- if (fi !== -1)
- floating.splice(fi, 1);
+ removeFromList(floating, w);
+ removeFromList(suspended, w);
managed.push(w);
const layout = layoutFor(w);
insertWindow(layout, w);
+
+ if (!w.fullScreen) {
+ w.keepAbove = false;
+ w.keepBelow = true;
+ }
+
arrange(layout);
log("TILED: " + w.caption);
}
function detachWindow(w) {
- const index = managed.indexOf(w);
-
- if (index === -1)
+ if (!contains(managed, w))
return false;
- managed.splice(index, 1);
+ removeFromList(managed, w);
- for (const layout of layouts) {
- if (removeNode(layout, w)) {
- arrange(layout);
- break;
- }
+ const layout = findWindowLayout(w);
+
+ if (layout) {
+ removeNode(layout, w);
+ arrange(layout);
}
return true;
}
function floatWindow(w) {
- if (!detachWindow(w))
+ if (isIgnored(w))
return;
+ detachWindow(w);
+
+ removeFromList(suspended, w);
+
if (!contains(floating, w))
floating.push(w);
function toggleFloating() {
const w = workspace.activeWindow;
- if (!w || !w.normalWindow)
+ if (!isCandidate(w) || isIgnored(w))
return;
if (contains(managed, w)) {
floatWindow(w);
- } else {
+ } else if (!w.minimized) {
tileWindow(w);
}
}
-workspace.windowAdded.connect(function(w) {
- if (eligible(w))
+// ------------------------------------------------------------
+// Fullscreen handling
+// ------------------------------------------------------------
+
+function handleFullscreen(w) {
+ if (!contains(managed, w))
+ return;
+
+ if (w.fullScreen) {
+ // Temporarily release KWin's BelowLayer.
+ // The tree node stays reserved.
+ w.keepBelow = false;
+
+ log("FULLSCREEN: " + w.caption);
+ } else {
+ w.keepAbove = false;
+ w.keepBelow = true;
+
+ const layout = findWindowLayout(w);
+
+ if (layout)
+ arrange(layout);
+
+ log("FULLSCREEN EXIT: " + w.caption);
+ }
+}
+
+// ------------------------------------------------------------
+// Minimize handling
+// ------------------------------------------------------------
+
+function handleMinimized(w) {
+ if (w.minimized) {
+ if (!contains(managed, w))
+ return;
+
+ detachWindow(w);
+
+ w.keepBelow = false;
+
+ if (!contains(suspended, w))
+ suspended.push(w);
+
+ log("MINIMIZED: " + w.caption);
+ } else {
+ if (!contains(suspended, w))
+ return;
+
+ removeFromList(suspended, w);
+
tileWindow(w);
-});
-workspace.windowRemoved.connect(function(w) {
+ log("RESTORED: " + w.caption);
+ }
+}
+
+// ------------------------------------------------------------
+// Window lifecycle
+// ------------------------------------------------------------
+
+function watchWindow(w) {
+ if (!w || contains(watched, w))
+ return;
+
+ watched.push(w);
+
+ w.fullScreenChanged.connect(function() {
+ handleFullscreen(w);
+ });
+
+ w.minimizedChanged.connect(function() {
+ handleMinimized(w);
+ });
+}
+
+function handleWindowAdded(w) {
+ log(
+ "WINDOW ADDED: " +
+ "caption=" + w.caption +
+ " class=" + w.resourceClass +
+ " normal=" + w.normalWindow +
+ " transient=" + Boolean(w.transient) +
+ " id=" + w.internalId
+ );
+ if (!isCandidate(w) || isIgnored(w))
+ return;
+
+ watchWindow(w);
+
+ if (isForcedFloating(w)) {
+ floatWindow(w);
+ return;
+ }
+
+ if (w.minimized) {
+ suspended.push(w);
+ return;
+ }
+
+ tileWindow(w);
+}
+
+function handleWindowRemoved(w) {
detachWindow(w);
- const index = floating.indexOf(w);
- if (index !== -1)
- floating.splice(index, 1);
-});
+ removeFromList(floating, w);
+ removeFromList(suspended, w);
+ removeFromList(watched, w);
+}
+
+// ------------------------------------------------------------
+// Events
+// ------------------------------------------------------------
+
+workspace.windowAdded.connect(handleWindowAdded);
+workspace.windowRemoved.connect(handleWindowRemoved);
workspace.screensChanged.connect(function() {
for (const layout of layouts)
arrange(layout);
});
+// ------------------------------------------------------------
+// Shortcut
+// ------------------------------------------------------------
+
registerShortcut(
"SimpleDwindleToggleFloating",
"SimpleDwindle: Toggle Floating",
toggleFloating
);
-log("Version 0.2 initialized");
+registerShortcut(
+ "SimpleDwindleListWindows",
+ "SimpleDwindle: List Windows",
+ "Meta+Shift+I",
+ function () {
+ log("========== WINDOW LIST ==========");
+
+ for (const w of workspace.windowList()) {
+ log(
+ "class=" + w.resourceClass +
+ " | caption=" + w.caption +
+ " | normal=" + w.normalWindow +
+ " | transient=" + Boolean(w.transient) +
+ " | minimized=" + w.minimized +
+ " | id=" + w.internalId
+ );
+ }
+
+ log("=================================");
+ }
+);
+
+log("Version " + VERSION + " initialized");