// SPDX-FileCopyrightText: 2026 Marco Allegretti // SPDX-License-Identifier: EUPL-1.2 // // SHIFT Dynamic Tiling — KWin declarative script // // Architecture: // - One persistent layout tree per output, keyed by output.name // - Tree nodes are split groups or window leaves; leaf rects are absolute // in-screen coordinates // - On add/remove/reorder the tree is updated and then laid out for the // affected screen, preserving the split structure instead of rebuilding a // fresh linear BSP from window count // - Drag detection uses interactiveMoveResizeStarted/Stepped/Finished // - Dragging over a tile shows the exact directional split that will be // created on drop // - Gap: outer 8px on screen edges, inner 8px between tiles (4px each side) import QtQuick import org.kde.kwin as KWinComponents import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings Item { id: root // ── Configuration ─────────────────────────────────────────────────────── readonly property int outerGap: 8 readonly property int innerGap: 8 // half applied to each edge → 4px per tile // ── State ─────────────────────────────────────────────────────────────── // Per-screen layout tree. Key: output.name. Value: split/leaf node. property var screenLayouts: ({}) // Per-screen tile list derived from screenLayouts. Key: output.name. // Value: [{win, rect}], where rect is absolute in-screen coordinates. property var screenTiles: ({}) // Windows the user has manually floated (by UUID string). property var floatedWindows: ({}) // Windows whose interactive move/resize signals are already connected. property var dragConnectedWindows: ({}) property int lastWindowRequestSerial: ShellSettings.Settings.dynamicTilingWindowRequestSerial // Drag state. // // Behaviour: dragging a tile over another tile previews the exact // left/right/up/down split that will be created on drop. Dropping inside // the work area but outside another tile restores the original slot. // Dropping outside the work area floats the window, giving users a visible // escape hatch from the tiled layout. property var draggingWindow: null property bool dragOutlineActive: false property string dragDropMode: "" // "insert", "restore", or "float" property rect dragOutlineRect: Qt.rect(0, 0, 0, 0) // Reorder state — kept stable while dragging so the rest of the layout // doesn't shuffle under the cursor. property string dragSourceScreen: "" property int dragSourceIndex: -1 property rect dragSourceRect: Qt.rect(0, 0, 0, 0) property var dragInsertTarget: null // {screen, key, direction, rect} preview slot readonly property int floatEscapeMargin: 32 // Deferred retile queue. // The dockSpaceReserver LayerShell exclusive zone needs one Wayland // roundtrip after setMaximize() before KWin updates MaximizeArea. // We queue output names and flush after 200 ms — same pattern as // convergentwindows constrainAfterRestoreTimer. property var pendingRetile: [] Timer { id: retileTimer interval: 200 repeat: false onTriggered: { const queue = root.pendingRetile.slice(); root.pendingRetile = []; const done = {}; for (let i = 0; i < queue.length; i++) { if (!done[queue[i]]) { done[queue[i]] = true; root.retileScreen(queue[i]); } } } } function scheduleRetile(outputName) { const q = root.pendingRetile.slice(); if (q.indexOf(outputName) < 0) q.push(outputName); root.pendingRetile = q; retileTimer.restart(); } // ── Tiling guard ──────────────────────────────────────────────────────── // Active when convergence is on, gaming is off, AND the user has // dynamic tiling enabled in quick settings. When this returns false // the script is fully inert and KWin's native quick-tile behaviour // owns window placement. function isConvergence() { return ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled && ShellSettings.Settings.dynamicTilingEnabled; } // Mirror the same guard used by convergentwindows: only normalWindow is // reliable in the KWin 6 declarative script API. Add maximizable to // avoid calling setMaximize on layer-shell / panel surfaces. function shouldIgnore(win) { if (!win || win.deleted) return true; if (!win.normalWindow) return true; // panels, dock, desktop, layer-shell if (!win.maximizable) return true; // fixed/special surfaces if (win.fullScreen) return true; // Skip xwaylandvideobridge (same as convergentwindows) if (win.resourceClass === "xwaylandvideobridge") return true; return false; } function shouldFloat(win) { if (!win) return true; // Fixed-size windows (won't tile sensibly) const minW = win.minSize ? win.minSize.width : 0; const maxW = win.maxSize ? win.maxSize.width : 0; const minH = win.minSize ? win.minSize.height : 0; const maxH = win.maxSize ? win.maxSize.height : 0; if (minW > 0 && maxW > 0 && minW >= maxW) return true; if (minH > 0 && maxH > 0 && minH >= maxH) return true; // Manually floated if (floatedWindows[windowKey(win)]) return true; return false; } function isTileable(win) { if (!isConvergence()) return false; if (shouldIgnore(win)) return false; if (shouldFloat(win)) return false; return true; } // ── Layout engine ─────────────────────────────────────────────────────── function workRect(win) { const output = win.output; const desktop = win.desktops[0]; if (!output || !desktop) return null; return KWinComponents.Workspace.clientArea( KWinComponents.Workspace.MaximizeArea, output, desktop); } // Apply outer + inner gaps to a list of rects that together tile a screen. // outer: gap between screen edge and tile // inner: total gap between two adjacent tiles (split equally, so 4px each side) function applyGaps(rects, workArea) { if (!rects || rects.length === 0) return rects; const half = innerGap / 2; const result = []; for (let i = 0; i < rects.length; i++) { let r = rects[i]; // Determine which edges touch the work area boundary const atLeft = Math.abs(r.x - workArea.x) < 2; const atTop = Math.abs(r.y - workArea.y) < 2; const atRight = Math.abs((r.x + r.width) - (workArea.x + workArea.width)) < 2; const atBottom = Math.abs((r.y + r.height) - (workArea.y + workArea.height)) < 2; const left = atLeft ? outerGap : half; const top = atTop ? outerGap : half; const right = atRight ? outerGap : half; const bottom = atBottom ? outerGap : half; result.push(Qt.rect( r.x + left, r.y + top, r.width - left - right, r.height - top - bottom )); } return result; } function setScreenLayout(outputName, layout) { const layouts = Object.assign({}, screenLayouts); if (layout) { layouts[outputName] = layout; } else { delete layouts[outputName]; } screenLayouts = layouts; } function setScreenTiles(outputName, tiles) { const allTiles = Object.assign({}, screenTiles); if (tiles && tiles.length > 0) { allTiles[outputName] = tiles; } else { delete allTiles[outputName]; } screenTiles = allTiles; } function makeLeaf(win) { return { kind: "leaf", win: win, rect: Qt.rect(0, 0, 0, 0) }; } function makeSplit(orientation, first, second, ratio) { return { kind: "split", orientation: orientation, first: first, second: second, ratio: ratio || 0.5, rect: Qt.rect(0, 0, 0, 0) }; } function splitOrientation(rect) { return rect && rect.width >= rect.height ? "vertical" : "horizontal"; } function directionOrientation(direction, rect) { if (direction === "left" || direction === "right") return "vertical"; if (direction === "up" || direction === "down") return "horizontal"; return splitOrientation(rect); } function splitRect(rect, orientation, ratio) { const splitRatio = ratio || 0.5; if (orientation === "vertical") { const firstWidth = Math.round(rect.width * splitRatio); return [ Qt.rect(rect.x, rect.y, firstWidth, rect.height), Qt.rect(rect.x + firstWidth, rect.y, rect.width - firstWidth, rect.height) ]; } const firstHeight = Math.round(rect.height * splitRatio); return [ Qt.rect(rect.x, rect.y, rect.width, firstHeight), Qt.rect(rect.x, rect.y + firstHeight, rect.width, rect.height - firstHeight) ]; } function collectLeaves(node, leaves) { if (!node) return; if (node.kind === "leaf") { leaves.push(node); return; } collectLeaves(node.first, leaves); collectLeaves(node.second, leaves); } function lastLeafKey(node) { if (!node) return ""; if (node.kind === "leaf") return windowKey(node.win); return lastLeafKey(node.second) || lastLeafKey(node.first); } function containsLeaf(node, key) { if (!node || !key) return false; if (node.kind === "leaf") return windowKey(node.win) === key; return containsLeaf(node.first, key) || containsLeaf(node.second, key); } function splitLeaf(node, targetKey, newLeaf, direction, fallbackRect) { if (!node || !targetKey || !newLeaf) return node; if (node.kind === "leaf") { if (windowKey(node.win) !== targetKey) return node; const baseRect = validRect(node.rect) ? node.rect : fallbackRect; const orientation = directionOrientation(direction, baseRect); if (direction === "left" || direction === "up") { return makeSplit(orientation, newLeaf, node, 0.5); } return makeSplit(orientation, node, newLeaf, 0.5); } node.first = splitLeaf(node.first, targetKey, newLeaf, direction, fallbackRect); node.second = splitLeaf(node.second, targetKey, newLeaf, direction, fallbackRect); return node; } function removeLeaf(node, key) { if (!node || !key) return { node: node, leaf: null }; if (node.kind === "leaf") { if (windowKey(node.win) === key) { return { node: null, leaf: node }; } return { node: node, leaf: null }; } let first = removeLeaf(node.first, key); if (first.leaf) { node.first = first.node; if (!node.first) return { node: node.second, leaf: first.leaf }; return { node: node, leaf: first.leaf }; } let second = removeLeaf(node.second, key); if (second.leaf) { node.second = second.node; if (!node.second) return { node: node.first, leaf: second.leaf }; return { node: node, leaf: second.leaf }; } return { node: node, leaf: null }; } function layoutTree(node, area, leaves) { if (!node || !area) return; node.rect = Qt.rect(area.x, area.y, area.width, area.height); if (node.kind === "leaf") { leaves.push(node); return; } const rects = splitRect(area, node.orientation, node.ratio); layoutTree(node.first, rects[0], leaves); layoutTree(node.second, rects[1], leaves); } function insertWindowInLayout(outputName, win) { const rootNode = screenLayouts[outputName]; const newLeaf = makeLeaf(win); if (!rootNode) { setScreenLayout(outputName, newLeaf); return; } let targetKey = ""; const active = KWinComponents.Workspace.activeWindow; if (active && active.output && active.output.name === outputName && windowKey(active) !== windowKey(win)) { targetKey = windowKey(active); } if (!containsLeaf(rootNode, targetKey)) { targetKey = lastLeafKey(rootNode); } setScreenLayout(outputName, splitLeaf(rootNode, targetKey, newLeaf, "", workRect(win))); } function removeWindowFromLayout(outputName, win) { const key = windowKey(win); const rootNode = screenLayouts[outputName]; if (!key || !rootNode) return false; const result = removeLeaf(rootNode, key); if (!result.leaf) return false; setScreenLayout(outputName, result.node); return true; } function moveWindowToSplit(target) { if (!target || !draggingWindow || !dragSourceScreen) return; const sourceKey = windowKey(draggingWindow); if (!sourceKey || sourceKey === target.key) return; const sourceRoot = screenLayouts[dragSourceScreen]; const detached = removeLeaf(sourceRoot, sourceKey); if (!detached.leaf) return; setScreenLayout(dragSourceScreen, detached.node); const targetRoot = screenLayouts[target.screen]; if (!targetRoot || !containsLeaf(targetRoot, target.key)) { setScreenLayout(dragSourceScreen, sourceRoot); return; } setScreenLayout(target.screen, splitLeaf(targetRoot, target.key, detached.leaf, target.direction, target.rect)); retileScreen(dragSourceScreen); if (target.screen !== dragSourceScreen) { retileScreen(target.screen); } } // Recompute and apply layout for a single screen. function retileScreen(outputName) { const rootNode = screenLayouts[outputName]; if (!rootNode) { setScreenTiles(outputName, []); return; } const leaves = []; collectLeaves(rootNode, leaves); if (leaves.length === 0) { setScreenTiles(outputName, []); return; } // Get work area from the first window's output let area = null; for (let i = 0; i < leaves.length; i++) { const r = workRect(leaves[i].win); if (r) { area = r; break; } } if (!area) return; const rawLeaves = []; layoutTree(rootNode, area, rawLeaves); const rawRects = []; for (let i = 0; i < rawLeaves.length; i++) { rawRects.push(rawLeaves[i].rect); } const gappedRects = applyGaps(rawRects, area); for (let i = 0; i < rawLeaves.length; i++) { const win = rawLeaves[i].win; if (!win || win.deleted) continue; const r = gappedRects[i]; rawLeaves[i].rect = r; win.frameGeometry = r; } setScreenLayout(outputName, rootNode); setScreenTiles(outputName, rawLeaves.slice()); } // Retile all layout trees. function retileAll() { for (const name in screenLayouts) { retileScreen(name); } } function scheduleRetileAll() { for (const name in screenLayouts) { scheduleRetile(name); } } function screenNameForWindow(win) { const key = windowKey(win); if (!key) return ""; for (const name in screenLayouts) { if (containsLeaf(screenLayouts[name], key)) { return name; } } return ""; } function reconcileWindowOutput(win) { if (!isTileable(win)) return; connectDragHandlers(win); const output = win.output; if (!output) return; const currentName = screenNameForWindow(win); const targetName = output.name; if (currentName !== "" && currentName !== targetName) { removeWindowFromLayout(currentName, win); scheduleRetile(currentName); } if (!containsLeaf(screenLayouts[targetName], windowKey(win))) { insertWindowInLayout(targetName, win); } if (win.maximizable) win.setMaximize(false, false); win.noBorder = false; scheduleRetile(targetName); } // Add a window to its screen's tile list and retile. function addWindow(win) { if (!isTileable(win)) return; connectDragHandlers(win); const output = win.output; if (!output) return; const name = output.name; // Avoid duplicates const key = windowKey(win); if (containsLeaf(screenLayouts[name], key)) return; const tiles = screenTiles[name] || []; for (let i = 0; i < tiles.length; i++) { if (windowKey(tiles[i].win) === key) return; } insertWindowInLayout(name, win); // Un-maximize now so the exclusive-zone Wayland roundtrip begins; // retileScreen runs 200 ms later when MaximizeArea has settled. // (Same pattern as convergentwindows constrainAfterRestoreTimer.) if (win.maximizable) win.setMaximize(false, false); win.noBorder = false; scheduleRetile(name); } // Remove a window from its screen's tile list and retile. function removeWindow(win) { if (!win) return; // Search all layout trees (window may have been moved or queued before // the deferred tile list has refreshed). for (const sName in screenLayouts) { if (removeWindowFromLayout(sName, win)) { retileScreen(sName); return; } } } function windowKey(win) { return win && win.internalId !== undefined ? String(win.internalId) : ""; } function findWindowById(windowId) { const wanted = String(windowId); const wins = KWinComponents.Workspace.windows; for (let i = 0; i < wins.length; i++) { if (windowKey(wins[i]) === wanted) { return wins[i]; } } return null; } function floatWindow(win) { const key = windowKey(win); if (!key) return; floatedWindows[key] = true; removeWindow(win); } function tileWindow(win) { const key = windowKey(win); if (!key) return; delete floatedWindows[key]; if (isTileable(win)) { addWindow(win); } } function connectDragHandlers(win) { const key = windowKey(win); if (!key || dragConnectedWindows[key]) return; dragConnectedWindows[key] = true; win.interactiveMoveResizeStarted.connect(function() { root.onDragStart(win); }); win.interactiveMoveResizeStepped.connect(function(geo) { root.onDragStep(win, geo); }); win.interactiveMoveResizeFinished.connect(function() { root.onDragEnd(win); }); } function handleWindowTilingRequest() { const serial = ShellSettings.Settings.dynamicTilingWindowRequestSerial; if (serial === lastWindowRequestSerial) return; lastWindowRequestSerial = serial; if (!isConvergence()) return; const win = findWindowById(ShellSettings.Settings.dynamicTilingWindowRequestId); if (!win || shouldIgnore(win)) return; const action = ShellSettings.Settings.dynamicTilingWindowRequestAction; if (action === "float") { floatWindow(win); } else if (action === "tile") { tileWindow(win); } } // ── Keyboard navigation helpers ────────────────────────────────────────── function centreOf(rect) { return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }; } // Find the tile on-screen whose centre is most in `direction` from `fromRect`. // direction: "left"|"right"|"up"|"down" function findNeighbour(fromWin, direction) { const outputName = fromWin.output ? fromWin.output.name : null; if (!outputName) return null; const tiles = screenTiles[outputName]; if (!tiles) return null; const from = fromWin.frameGeometry; const fc = centreOf(from); let best = null; let bestScore = Infinity; for (let i = 0; i < tiles.length; i++) { const t = tiles[i]; if (windowKey(t.win) === windowKey(fromWin)) continue; const tc = centreOf(t.rect); const dx = tc.x - fc.x; const dy = tc.y - fc.y; let inDirection = false; let primary = 0; let secondary = 0; switch (direction) { case "left": inDirection = dx < -5; primary = -dx; secondary = Math.abs(dy); break; case "right": inDirection = dx > 5; primary = dx; secondary = Math.abs(dy); break; case "up": inDirection = dy < -5; primary = -dy; secondary = Math.abs(dx); break; case "down": inDirection = dy > 5; primary = dy; secondary = Math.abs(dx); break; } if (!inDirection) continue; // Score: penalise perpendicular distance lightly const score = primary + secondary * 0.3; if (score < bestScore) { bestScore = score; best = t.win; } } return best; } // ── Workspace connections ───────────────────────────────────────────── Connections { target: KWinComponents.Workspace function onWindowAdded(win) { if (isTileable(win)) { addWindow(win); } } function onWindowRemoved(win) { const key = root.windowKey(win); if (key) delete root.dragConnectedWindows[key]; root.removeWindow(win); } function onScreensChanged() { if (!root.isConvergence()) { return; } const wins = KWinComponents.Workspace.windows; for (let i = 0; i < wins.length; i++) { root.reconcileWindowOutput(wins[i]); } root.scheduleRetileAll(); } } Connections { target: ShellSettings.Settings function onConvergenceModeEnabledChanged() { if (isConvergence()) { // Tile all existing normal windows const wins = KWinComponents.Workspace.windows; for (let i = 0; i < wins.length; i++) { addWindow(wins[i]); } } else { // Clear all tiles — the convergentwindows script will re-maximize screenLayouts = {}; screenTiles = {}; } } function onGamingModeEnabledChanged() { if (ShellSettings.Settings.gamingModeEnabled) { screenLayouts = {}; screenTiles = {}; } else if (isConvergence()) { const wins = KWinComponents.Workspace.windows; for (let i = 0; i < wins.length; i++) { addWindow(wins[i]); } } } function onDynamicTilingEnabledChanged() { if (isConvergence()) { const wins = KWinComponents.Workspace.windows; for (let i = 0; i < wins.length; i++) { addWindow(wins[i]); } } else { // Tiling turned off — leave windows where they are. screenLayouts = {}; screenTiles = {}; } } function onDynamicTilingWindowRequestChanged() { root.handleWindowTilingRequest(); } } // ── Drag handlers ───────────────────────────────────────────────────── // Find the (screen, index) of an existing tile holding this window. function findTileSlot(win) { for (const sName in screenTiles) { const tiles = screenTiles[sName]; for (let i = 0; i < tiles.length; i++) { if (tiles[i].win && windowKey(tiles[i].win) === windowKey(win)) { return { screen: sName, index: i }; } } } return null; } // Find the tile under a cursor position, ignoring the dragged window. function findTileAtCursor(cursor, ignoreWin) { for (const sName in screenTiles) { const tiles = screenTiles[sName]; for (let i = 0; i < tiles.length; i++) { const t = tiles[i]; if (ignoreWin && t.win && windowKey(t.win) === windowKey(ignoreWin)) continue; const r = t.rect; if (!r || r.width <= 0 || r.height <= 0) continue; if (cursor.x >= r.x && cursor.x <= r.x + r.width && cursor.y >= r.y && cursor.y <= r.y + r.height) { return { screen: sName, index: i, rect: r, key: windowKey(t.win), win: t.win }; } } } return null; } function tileInsertDirection(cursor, rect) { if (!validRect(rect)) return ""; const left = Math.abs(cursor.x - rect.x); const right = Math.abs((rect.x + rect.width) - cursor.x); const top = Math.abs(cursor.y - rect.y); const bottom = Math.abs((rect.y + rect.height) - cursor.y); const nearest = Math.min(left, right, top, bottom); if (nearest === left) return "left"; if (nearest === right) return "right"; if (nearest === top) return "up"; return "down"; } function previewInsertRect(win, target, position) { if (!target || !validRect(target.rect) || dragSourceIndex < 0) return null; const orientation = directionOrientation(position, target.rect); const rects = splitRect(target.rect, orientation, 0.5); if (position === "left" || position === "up") return rects[0]; return rects[1]; } function insertDraggedWindow(target) { moveWindowToSplit(target); } function validRect(rect) { return rect && rect.width > 0 && rect.height > 0; } function rectContainsPoint(rect, point) { return validRect(rect) && point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } function rectsClose(a, b) { return Math.abs(a.x - b.x) < 2 && Math.abs(a.y - b.y) < 2 && Math.abs(a.width - b.width) < 2 && Math.abs(a.height - b.height) < 2; } function outsideWorkArea(win, cursor) { const area = workRect(win); if (!area) return false; const expanded = Qt.rect( area.x - floatEscapeMargin, area.y - floatEscapeMargin, area.width + floatEscapeMargin * 2, area.height + floatEscapeMargin * 2 ); return !rectContainsPoint(expanded, cursor); } function showDragOutline(mode, rect) { if (!validRect(rect)) { clearDragOutline(); return; } if (dragOutlineActive && dragDropMode === mode && rectsClose(dragOutlineRect, rect)) { return; } dragDropMode = mode; dragOutlineRect = Qt.rect(rect.x, rect.y, rect.width, rect.height); dragOutlineActive = true; } function clearDragOutline() { dragOutlineActive = false; dragDropMode = ""; dragOutlineRect = Qt.rect(0, 0, 0, 0); } function resetDragState() { clearDragOutline(); dragInsertTarget = null; dragSourceScreen = ""; dragSourceIndex = -1; dragSourceRect = Qt.rect(0, 0, 0, 0); draggingWindow = null; } function updateDragIntent(win, geo) { const cursor = KWinComponents.Workspace.cursorPos; const target = findTileAtCursor(cursor, win); if (target) { const position = tileInsertDirection(cursor, target.rect); const insertRect = previewInsertRect(win, target, position); if (validRect(insertRect)) { dragInsertTarget = { screen: target.screen, key: target.key, direction: position, rect: insertRect, position: position }; showDragOutline("insert", insertRect); return; } } dragInsertTarget = null; if (outsideWorkArea(win, cursor)) { showDragOutline("float", validRect(geo) ? geo : win.frameGeometry); } else { showDragOutline("restore", dragSourceRect); } } function onDragStart(win) { if (!isConvergence()) return; draggingWindow = win; clearDragOutline(); dragInsertTarget = null; dragSourceRect = Qt.rect(0, 0, 0, 0); // Remember the source slot so we can insert on drop. // The tile stays in screenTiles[] during the drag so the rest of // the layout doesn't shuffle. const slot = findTileSlot(win); if (slot) { dragSourceScreen = slot.screen; dragSourceIndex = slot.index; const tiles = screenTiles[slot.screen]; if (tiles && tiles[slot.index]) { const rect = tiles[slot.index].rect; dragSourceRect = Qt.rect(rect.x, rect.y, rect.width, rect.height); } } else { dragSourceScreen = ""; dragSourceIndex = -1; } showDragOutline("restore", dragSourceRect); } function onDragStep(win, geo) { if (!isConvergence()) { resetDragState(); return; } if (draggingWindow !== win) return; updateDragIntent(win, geo); } function onDragEnd(win) { if (!isConvergence()) { resetDragState(); return; } const finalDropMode = dragDropMode; clearDragOutline(); if (finalDropMode === "float") { floatWindow(win); } else if (finalDropMode === "insert" && dragInsertTarget) { insertDraggedWindow(dragInsertTarget); } // Dropped elsewhere → restore the source tile to its original slot. // (KWin's native quick-tile may have moved the window; retileScreen // sets frameGeometry back to the BSP rect so the layout stays intact.) else if (dragSourceScreen && dragSourceIndex >= 0) { retileScreen(dragSourceScreen); } resetDragState(); } // ── Keyboard shortcuts ───────────────────────────────────────────────── // Focus navigation KWinComponents.ShortcutHandler { name: "SHIFT Tiling Focus Left" text: "SHIFT Tiling: Focus window to the left" sequence: "Meta+H" onActivated: { const win = KWinComponents.Workspace.activeWindow; if (!win) return; const target = root.findNeighbour(win, "left"); if (target) KWinComponents.Workspace.activeWindow = target; } } KWinComponents.ShortcutHandler { name: "SHIFT Tiling Focus Right" text: "SHIFT Tiling: Focus window to the right" sequence: "Meta+L" onActivated: { const win = KWinComponents.Workspace.activeWindow; if (!win) return; const target = root.findNeighbour(win, "right"); if (target) KWinComponents.Workspace.activeWindow = target; } } KWinComponents.ShortcutHandler { name: "SHIFT Tiling Focus Up" text: "SHIFT Tiling: Focus window above" sequence: "Meta+K" onActivated: { const win = KWinComponents.Workspace.activeWindow; if (!win) return; const target = root.findNeighbour(win, "up"); if (target) KWinComponents.Workspace.activeWindow = target; } } KWinComponents.ShortcutHandler { name: "SHIFT Tiling Focus Down" text: "SHIFT Tiling: Focus window below" sequence: "Meta+J" onActivated: { const win = KWinComponents.Workspace.activeWindow; if (!win) return; const target = root.findNeighbour(win, "down"); if (target) KWinComponents.Workspace.activeWindow = target; } } // Float toggle KWinComponents.ShortcutHandler { name: "SHIFT Tiling Float Toggle" text: "SHIFT Tiling: Toggle float for active window" sequence: "Meta+F" onActivated: { const win = KWinComponents.Workspace.activeWindow; if (!win) return; const id = root.windowKey(win); if (root.floatedWindows[id]) { root.tileWindow(win); } else { root.floatWindow(win); } } } // Tiling on/off KWinComponents.ShortcutHandler { name: "SHIFT Tiling Toggle" text: "SHIFT Tiling: Toggle tiling on/off" sequence: "Meta+T" onActivated: { ShellSettings.Settings.dynamicTilingEnabled = !ShellSettings.Settings.dynamicTilingEnabled; } } // ── Component setup ─────────────────────────────────────────────────── Component.onCompleted: { // Connect to existing windows const wins = KWinComponents.Workspace.windows; for (let i = 0; i < wins.length; i++) { const win = wins[i]; if (isTileable(win)) { addWindow(win); } } } }