Page dynamic tiles by workspace

Keep one layout per output and virtual desktop. Move new windows to another existing desktop when the current page is full.

When a user moves a window onto a full page, replace the last focused window on that page and send it back to the source desktop. Use stable slot swaps instead of insert splits so the layout shape does not change during moves.
This commit is contained in:
Marco Allegretti 2026-05-20 09:38:37 +02:00
parent 40beabede9
commit 75b9049a8c
2 changed files with 361 additions and 149 deletions

View file

@ -4,16 +4,19 @@
// SHIFT Dynamic Tiling KWin declarative script // SHIFT Dynamic Tiling KWin declarative script
// //
// Architecture: // Architecture:
// - One persistent layout tree per output, keyed by output.name // - One persistent layout tree per output and virtual desktop
// - Tree nodes are split groups or window leaves; leaf rects are absolute // - Tree nodes are split groups or window leaves; leaf rects are absolute
// in-screen coordinates // in-screen coordinates
// - Automatic add/remove rebuilds a stable master-stack tree from window // - Automatic add/remove rebuilds a stable master-stack tree from window
// order, so normal app churn does not leave arbitrary nested splits behind // order, so normal app churn does not leave arbitrary nested splits behind
// - Explicit drag insertion still edits the tree directly because that is a // - If the current page is full, new windows move to another existing
// deliberate user layout change // virtual desktop with free capacity
// - The current screen can cycle between master-stack, equal columns, and
// equal rows without changing window order
// - Dragging over an existing tile swaps the two window leaves without
// changing the split tree shape
// - Drag detection uses interactiveMoveResizeStarted/Stepped/Finished // - Drag detection uses interactiveMoveResizeStarted/Stepped/Finished
// - Dragging over a tile shows the exact directional split that will be // - Dragging over a tile previews the slot that will be swapped on drop
// created on drop
// - Gap: outer 8px on screen edges, inner 8px between tiles (4px each side) // - Gap: outer 8px on screen edges, inner 8px between tiles (4px each side)
import QtQuick import QtQuick
@ -26,18 +29,29 @@ Item {
// Configuration // Configuration
readonly property int outerGap: 8 readonly property int outerGap: 8
readonly property int innerGap: 8 // half applied to each edge 4px per tile readonly property int innerGap: 8 // half applied to each edge 4px per tile
readonly property int maxWindowsPerPage: 4
readonly property real stablePrimaryRatio: 0.58 readonly property real stablePrimaryRatio: 0.58
readonly property real insertIntentDeadZone: 0.18 readonly property var layoutModes: ["master", "columns", "rows"]
// State // State
// Per-screen layout tree. Key: output.name. Value: split/leaf node. // Per-screen/per-desktop layout tree. Key: output.name + desktop key.
// Value: split/leaf node.
property var screenLayouts: ({}) property var screenLayouts: ({})
// Per-screen tile list derived from screenLayouts. Key: output.name. // Per-screen/per-desktop tile list derived from screenLayouts.
// Value: [{win, rect}], where rect is absolute in-screen coordinates. // Value: [{win, rect}], where rect is absolute in-screen coordinates.
property var screenTiles: ({}) property var screenTiles: ({})
// Per-screen/per-desktop layout mode. Key: output.name + desktop key.
// Value: one of layoutModes.
property var screenLayoutModes: ({})
// Last focused tiled window per screen/desktop page. Used as the
// replacement target when a user manually moves another window onto a
// full page.
property var lastFocusedWindowKeys: ({})
// Windows the user has manually floated (by UUID string). // Windows the user has manually floated (by UUID string).
property var floatedWindows: ({}) property var floatedWindows: ({})
@ -48,14 +62,14 @@ Item {
// Drag state. // Drag state.
// //
// Behaviour: dragging a tile over another tile previews the exact // Behaviour: dragging a tile over another tile previews the target slot
// left/right/up/down split that will be created on drop. Dropping inside // that will be swapped on drop. Dropping inside
// the work area but outside another tile restores the original slot. // the work area but outside another tile restores the original slot.
// Dropping outside the work area floats the window, giving users a visible // Dropping outside the work area floats the window, giving users a visible
// escape hatch from the tiled layout. // escape hatch from the tiled layout.
property var draggingWindow: null property var draggingWindow: null
property bool dragOutlineActive: false property bool dragOutlineActive: false
property string dragDropMode: "" // "insert", "restore", or "float" property string dragDropMode: "" // "swap", "restore", or "float"
property rect dragOutlineRect: Qt.rect(0, 0, 0, 0) property rect dragOutlineRect: Qt.rect(0, 0, 0, 0)
// Reorder state kept stable while dragging so the rest of the layout // Reorder state kept stable while dragging so the rest of the layout
@ -63,7 +77,7 @@ Item {
property string dragSourceScreen: "" property string dragSourceScreen: ""
property int dragSourceIndex: -1 property int dragSourceIndex: -1
property rect dragSourceRect: Qt.rect(0, 0, 0, 0) property rect dragSourceRect: Qt.rect(0, 0, 0, 0)
property var dragInsertTarget: null // {screen, key, direction, rect} preview slot property var dragSwapTarget: null // {screen, key, rect} preview slot
readonly property int floatEscapeMargin: 32 readonly property int floatEscapeMargin: 32
// Deferred retile queue. // Deferred retile queue.
@ -97,6 +111,101 @@ Item {
retileTimer.restart(); retileTimer.restart();
} }
function desktopKey(desktop) {
if (!desktop) return "";
if (desktop.x11DesktopNumber !== undefined) return String(desktop.x11DesktopNumber);
const desktops = KWinComponents.Workspace.desktops;
const index = desktops ? desktops.indexOf(desktop) : -1;
return index >= 0 ? String(index + 1) : String(desktop);
}
function desktopForWindow(win) {
if (win && win.desktops && win.desktops.length > 0) {
return win.desktops[0];
}
return KWinComponents.Workspace.currentDesktop;
}
function layoutKeyFor(outputName, desktop) {
const key = desktopKey(desktop);
return outputName && key !== "" ? outputName + "::" + key : "";
}
function desktopForLayoutKey(outputName) {
const separator = outputName.lastIndexOf("::");
if (separator < 0) return null;
const wantedKey = outputName.slice(separator + 2);
const desktops = KWinComponents.Workspace.desktops;
if (!desktops) return null;
for (let i = 0; i < desktops.length; i++) {
if (desktopKey(desktops[i]) === wantedKey) {
return desktops[i];
}
}
return null;
}
function layoutKeyForWindow(win) {
if (!win || !win.output) return "";
return layoutKeyFor(win.output.name, desktopForWindow(win));
}
function assignWindowToDesktop(win, desktop) {
if (!win || !desktop) return;
if (win.desktops && win.desktops.length === 1 && win.desktops[0] === desktop) return;
win.desktops = [desktop];
}
function windowCountForLayout(outputName) {
const leaves = [];
collectLeaves(screenLayouts[outputName], leaves);
let count = 0;
for (let i = 0; i < leaves.length; i++) {
const win = leaves[i].win;
if (win && !win.deleted && isTileable(win) && layoutKeyForWindow(win) === outputName) {
count++;
}
}
return count;
}
function targetLayoutForNewWindow(win) {
const currentDesktop = desktopForWindow(win);
const currentKey = layoutKeyForWindow(win);
if (!win || !win.output || currentKey === "") {
return { key: "", desktop: null };
}
if (windowCountForLayout(currentKey) < maxWindowsPerPage) {
return { key: currentKey, desktop: currentDesktop };
}
const desktops = KWinComponents.Workspace.desktops;
if (!desktops || desktops.length < 2) {
return { key: currentKey, desktop: currentDesktop };
}
let startIndex = desktops.indexOf(currentDesktop);
if (startIndex < 0) {
startIndex = desktops.indexOf(KWinComponents.Workspace.currentDesktop);
}
if (startIndex < 0) {
startIndex = 0;
}
for (let offset = 1; offset < desktops.length; offset++) {
const desktop = desktops[(startIndex + offset) % desktops.length];
const key = layoutKeyFor(win.output.name, desktop);
if (key !== "" && windowCountForLayout(key) < maxWindowsPerPage) {
return { key: key, desktop: desktop };
}
}
return { key: currentKey, desktop: currentDesktop };
}
// Tiling guard // Tiling guard
// Active when convergence is on, gaming is off, AND the user has // Active when convergence is on, gaming is off, AND the user has
@ -147,7 +256,7 @@ Item {
function workRect(win) { function workRect(win) {
const output = win.output; const output = win.output;
const desktop = win.desktops[0]; const desktop = desktopForWindow(win);
if (!output || !desktop) return null; if (!output || !desktop) return null;
return KWinComponents.Workspace.clientArea( return KWinComponents.Workspace.clientArea(
KWinComponents.Workspace.MaximizeArea, output, desktop); KWinComponents.Workspace.MaximizeArea, output, desktop);
@ -218,16 +327,6 @@ Item {
}; };
} }
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) { function splitRect(rect, orientation, ratio) {
const splitRatio = ratio || 0.5; const splitRatio = ratio || 0.5;
if (orientation === "vertical") { if (orientation === "vertical") {
@ -255,10 +354,49 @@ Item {
collectLeaves(node.second, leaves); collectLeaves(node.second, leaves);
} }
function lastLeafKey(node) { function leafIndexForWindow(outputName, win) {
if (!node) return ""; const key = windowKey(win);
if (node.kind === "leaf") return windowKey(node.win); if (!key) return -1;
return lastLeafKey(node.second) || lastLeafKey(node.first);
const leaves = [];
collectLeaves(screenLayouts[outputName], leaves);
for (let i = 0; i < leaves.length; i++) {
if (windowKey(leaves[i].win) === key) {
return i;
}
}
return -1;
}
function leafForSlot(outputName, preferredIndex) {
const leaves = [];
collectLeaves(screenLayouts[outputName], leaves);
if (leaves.length === 0) return null;
if (preferredIndex >= 0 && preferredIndex < leaves.length) {
return leaves[preferredIndex];
}
return leaves[leaves.length - 1];
}
function rememberFocusedWindow(win) {
if (!isTileable(win)) return;
const outputName = layoutKeyForWindow(win);
const key = windowKey(win);
if (!outputName || !key || !containsLeaf(screenLayouts[outputName], key)) return;
const focused = Object.assign({}, lastFocusedWindowKeys);
focused[outputName] = key;
lastFocusedWindowKeys = focused;
}
function replacementLeafForLayout(outputName, fallbackIndex) {
const focusedKey = lastFocusedWindowKeys[outputName] || "";
const focusedLeaf = focusedKey ? findLeaf(screenLayouts[outputName], focusedKey) : null;
if (focusedLeaf && focusedLeaf.win && layoutKeyForWindow(focusedLeaf.win) === outputName) {
return focusedLeaf;
}
return leafForSlot(outputName, fallbackIndex);
} }
function orderedWindowsForScreen(outputName) { function orderedWindowsForScreen(outputName) {
@ -271,7 +409,7 @@ Item {
const win = leaves[i].win; const win = leaves[i].win;
const key = windowKey(win); const key = windowKey(win);
if (!key || seen[key] || !isTileable(win)) continue; if (!key || seen[key] || !isTileable(win)) continue;
if (!win.output || win.output.name !== outputName) continue; if (layoutKeyForWindow(win) !== outputName) continue;
seen[key] = true; seen[key] = true;
windows.push(win); windows.push(win);
} }
@ -306,9 +444,40 @@ Item {
); );
} }
function buildStableLayout(windows) { function buildLinearLayout(windows, startIndex, orientation) {
const remaining = windows.length - startIndex;
if (remaining <= 0) return null;
if (remaining === 1) return makeLeaf(windows[startIndex]);
return makeSplit(
orientation,
makeLeaf(windows[startIndex]),
buildLinearLayout(windows, startIndex + 1, orientation),
1 / remaining
);
}
function layoutModeForScreen(outputName) {
const mode = screenLayoutModes[outputName];
return layoutModes.indexOf(mode) >= 0 ? mode : "master";
}
function setLayoutModeForScreen(outputName, mode) {
if (!outputName || layoutModes.indexOf(mode) < 0) return;
const modes = Object.assign({}, screenLayoutModes);
modes[outputName] = mode;
screenLayoutModes = modes;
}
function buildStableLayout(windows, mode) {
if (!windows || windows.length === 0) return null; if (!windows || windows.length === 0) return null;
if (windows.length === 1) return makeLeaf(windows[0]); if (windows.length === 1) return makeLeaf(windows[0]);
if (mode === "columns") {
return buildLinearLayout(windows, 0, "vertical");
}
if (mode === "rows") {
return buildLinearLayout(windows, 0, "horizontal");
}
if (windows.length === 2) { if (windows.length === 2) {
return makeSplit("vertical", makeLeaf(windows[0]), makeLeaf(windows[1]), 0.5); return makeSplit("vertical", makeLeaf(windows[0]), makeLeaf(windows[1]), 0.5);
} }
@ -322,7 +491,33 @@ Item {
} }
function setStableLayout(outputName, windows) { function setStableLayout(outputName, windows) {
setScreenLayout(outputName, buildStableLayout(windows)); setScreenLayout(outputName, buildStableLayout(windows, layoutModeForScreen(outputName)));
}
function outputNameForActiveWindow() {
const win = KWinComponents.Workspace.activeWindow;
const activeName = layoutKeyForWindow(win);
if (activeName !== "") return activeName;
for (const name in screenLayouts) {
return name;
}
return "";
}
function cycleLayoutMode(outputName) {
if (!outputName) return;
const currentMode = layoutModeForScreen(outputName);
const currentIndex = layoutModes.indexOf(currentMode);
const nextMode = layoutModes[(currentIndex + 1) % layoutModes.length];
setLayoutModeForScreen(outputName, nextMode);
const windows = orderedWindowsForScreen(outputName);
if (windows.length > 0) {
setStableLayout(outputName, windows);
retileScreen(outputName);
scheduleRetile(outputName);
}
} }
function containsLeaf(node, key) { function containsLeaf(node, key) {
@ -331,52 +526,6 @@ Item {
return containsLeaf(node.first, key) || containsLeaf(node.second, 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) { function layoutTree(node, area, leaves) {
if (!node || !area) return; if (!node || !area) return;
node.rect = Qt.rect(area.x, area.y, area.width, area.height); node.rect = Qt.rect(area.x, area.y, area.width, area.height);
@ -417,7 +566,7 @@ Item {
} }
if (!leafKey || seen[leafKey] || !isTileable(leafWindow)) continue; if (!leafKey || seen[leafKey] || !isTileable(leafWindow)) continue;
if (!leafWindow.output || leafWindow.output.name !== outputName) continue; if (layoutKeyForWindow(leafWindow) !== outputName) continue;
seen[leafKey] = true; seen[leafKey] = true;
remaining.push(leafWindow); remaining.push(leafWindow);
} }
@ -427,29 +576,28 @@ Item {
return true; return true;
} }
function moveWindowToSplit(target) { function swapWindowBetweenLayouts(win, sourceName, targetName) {
if (!target || !draggingWindow || !dragSourceScreen) return; if (!win || !sourceName || !targetName || sourceName === targetName) return false;
const sourceKey = windowKey(draggingWindow); const sourceDesktop = desktopForLayoutKey(sourceName);
if (!sourceKey || sourceKey === target.key) return; const sourceLeaf = findLeaf(screenLayouts[sourceName], windowKey(win));
if (!sourceDesktop || !sourceLeaf) return false;
const sourceRoot = screenLayouts[dragSourceScreen]; const sourceIndex = leafIndexForWindow(sourceName, win);
const detached = removeLeaf(sourceRoot, sourceKey); const targetLeaf = replacementLeafForLayout(targetName, sourceIndex);
if (!detached.leaf) return; if (!targetLeaf || !targetLeaf.win || windowKey(targetLeaf.win) === windowKey(win)) return false;
setScreenLayout(dragSourceScreen, detached.node); const displacedWindow = targetLeaf.win;
targetLeaf.win = win;
sourceLeaf.win = displacedWindow;
assignWindowToDesktop(displacedWindow, sourceDesktop);
const targetRoot = screenLayouts[target.screen]; retileScreen(sourceName);
if (!targetRoot || !containsLeaf(targetRoot, target.key)) { retileScreen(targetName);
setScreenLayout(dragSourceScreen, sourceRoot); scheduleRetile(sourceName);
return; scheduleRetile(targetName);
} KWinComponents.Workspace.activeWindow = win;
return true;
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. // Recompute and apply layout for a single screen.
@ -528,9 +676,13 @@ Item {
if (!output) return; if (!output) return;
const currentName = screenNameForWindow(win); const currentName = screenNameForWindow(win);
const targetName = output.name; const targetName = layoutKeyForWindow(win);
if (targetName === "") return;
if (currentName !== "" && currentName !== targetName) { if (currentName !== "" && currentName !== targetName) {
if (windowCountForLayout(targetName) >= maxWindowsPerPage && swapWindowBetweenLayouts(win, currentName, targetName)) {
return;
}
removeWindowFromLayout(currentName, win); removeWindowFromLayout(currentName, win);
scheduleRetile(currentName); scheduleRetile(currentName);
} }
@ -553,7 +705,15 @@ Item {
const output = win.output; const output = win.output;
if (!output) return; if (!output) return;
const name = output.name; const initialName = layoutKeyForWindow(win);
const target = targetLayoutForNewWindow(win);
if (target.key === "") return;
const name = target.key;
if (target.desktop && initialName !== name) {
assignWindowToDesktop(win, target.desktop);
KWinComponents.Workspace.currentDesktop = target.desktop;
}
// Avoid duplicates // Avoid duplicates
const key = windowKey(win); const key = windowKey(win);
@ -628,6 +788,9 @@ Item {
win.interactiveMoveResizeStarted.connect(function() { root.onDragStart(win); }); win.interactiveMoveResizeStarted.connect(function() { root.onDragStart(win); });
win.interactiveMoveResizeStepped.connect(function(geo) { root.onDragStep(win, geo); }); win.interactiveMoveResizeStepped.connect(function(geo) { root.onDragStep(win, geo); });
win.interactiveMoveResizeFinished.connect(function() { root.onDragEnd(win); }); win.interactiveMoveResizeFinished.connect(function() { root.onDragEnd(win); });
if (win.desktopsChanged !== undefined) {
win.desktopsChanged.connect(function() { root.reconcileWindowOutput(win); });
}
} }
function handleWindowTilingRequest() { function handleWindowTilingRequest() {
@ -650,7 +813,7 @@ Item {
function promoteWindow(win) { function promoteWindow(win) {
if (!isTileable(win)) return; if (!isTileable(win)) return;
const outputName = screenNameForWindow(win) || (win.output ? win.output.name : ""); const outputName = screenNameForWindow(win) || layoutKeyForWindow(win);
if (!outputName) return; if (!outputName) return;
const key = windowKey(win); const key = windowKey(win);
@ -684,7 +847,7 @@ Item {
// Find the tile on-screen whose centre is most in `direction` from `fromRect`. // Find the tile on-screen whose centre is most in `direction` from `fromRect`.
// direction: "left"|"right"|"up"|"down" // direction: "left"|"right"|"up"|"down"
function findNeighbour(fromWin, direction) { function findNeighbour(fromWin, direction) {
const outputName = fromWin.output ? fromWin.output.name : null; const outputName = layoutKeyForWindow(fromWin);
if (!outputName) return null; if (!outputName) return null;
const tiles = screenTiles[outputName]; const tiles = screenTiles[outputName];
if (!tiles) return null; if (!tiles) return null;
@ -735,6 +898,10 @@ Item {
root.removeWindow(win); root.removeWindow(win);
} }
function onActiveWindowChanged() {
root.rememberFocusedWindow(KWinComponents.Workspace.activeWindow);
}
function onScreensChanged() { function onScreensChanged() {
if (!root.isConvergence()) { if (!root.isConvergence()) {
return; return;
@ -763,6 +930,8 @@ Item {
// Clear all tiles the convergentwindows script will re-maximize // Clear all tiles the convergentwindows script will re-maximize
screenLayouts = {}; screenLayouts = {};
screenTiles = {}; screenTiles = {};
screenLayoutModes = {};
lastFocusedWindowKeys = {};
} }
} }
@ -770,6 +939,8 @@ Item {
if (ShellSettings.Settings.gamingModeEnabled) { if (ShellSettings.Settings.gamingModeEnabled) {
screenLayouts = {}; screenLayouts = {};
screenTiles = {}; screenTiles = {};
screenLayoutModes = {};
lastFocusedWindowKeys = {};
} else if (isConvergence()) { } else if (isConvergence()) {
const wins = KWinComponents.Workspace.windows; const wins = KWinComponents.Workspace.windows;
for (let i = 0; i < wins.length; i++) { for (let i = 0; i < wins.length; i++) {
@ -788,6 +959,8 @@ Item {
// Tiling turned off leave windows where they are. // Tiling turned off leave windows where they are.
screenLayouts = {}; screenLayouts = {};
screenTiles = {}; screenTiles = {};
screenLayoutModes = {};
lastFocusedWindowKeys = {};
} }
} }
@ -813,7 +986,9 @@ Item {
// Find the tile under a cursor position, ignoring the dragged window. // Find the tile under a cursor position, ignoring the dragged window.
function findTileAtCursor(cursor, ignoreWin) { function findTileAtCursor(cursor, ignoreWin) {
const activeName = dragSourceScreen || layoutKeyForWindow(ignoreWin);
for (const sName in screenTiles) { for (const sName in screenTiles) {
if (activeName && sName !== activeName) continue;
const tiles = screenTiles[sName]; const tiles = screenTiles[sName];
for (let i = 0; i < tiles.length; i++) { for (let i = 0; i < tiles.length; i++) {
const t = tiles[i]; const t = tiles[i];
@ -829,34 +1004,31 @@ Item {
return null; return null;
} }
function tileInsertDirection(cursor, rect) { function findLeaf(node, key) {
if (!validRect(rect)) return ""; if (!node || !key) return null;
if (node.kind === "leaf") {
const relativeX = (cursor.x - rect.x) / rect.width; return windowKey(node.win) === key ? node : null;
const relativeY = (cursor.y - rect.y) / rect.height;
const fromCenterX = relativeX - 0.5;
const fromCenterY = relativeY - 0.5;
if (Math.abs(fromCenterX) < insertIntentDeadZone && Math.abs(fromCenterY) < insertIntentDeadZone) {
return "";
} }
return findLeaf(node.first, key) || findLeaf(node.second, key);
if (Math.abs(fromCenterX) >= Math.abs(fromCenterY)) {
return fromCenterX < 0 ? "left" : "right";
}
return fromCenterY < 0 ? "up" : "down";
} }
function previewInsertRect(win, target, position) { function swapDraggedWindow(target) {
if (!target || !validRect(target.rect) || dragSourceIndex < 0) return null; if (!target || !draggingWindow || !dragSourceScreen) return;
const orientation = directionOrientation(position, target.rect); const sourceKey = windowKey(draggingWindow);
const rects = splitRect(target.rect, orientation, 0.5); if (!sourceKey || sourceKey === target.key || target.screen !== dragSourceScreen) return;
if (position === "left" || position === "up") return rects[0];
return rects[1];
}
function insertDraggedWindow(target) { const rootNode = screenLayouts[dragSourceScreen];
moveWindowToSplit(target); const sourceLeaf = findLeaf(rootNode, sourceKey);
const targetLeaf = findLeaf(rootNode, target.key);
if (!sourceLeaf || !targetLeaf) return;
const sourceWindow = sourceLeaf.win;
sourceLeaf.win = targetLeaf.win;
targetLeaf.win = sourceWindow;
retileScreen(dragSourceScreen);
scheduleRetile(dragSourceScreen);
KWinComponents.Workspace.activeWindow = sourceWindow;
} }
function validRect(rect) { function validRect(rect) {
@ -909,7 +1081,7 @@ Item {
function resetDragState() { function resetDragState() {
clearDragOutline(); clearDragOutline();
dragInsertTarget = null; dragSwapTarget = null;
dragSourceScreen = ""; dragSourceScreen = "";
dragSourceIndex = -1; dragSourceIndex = -1;
dragSourceRect = Qt.rect(0, 0, 0, 0); dragSourceRect = Qt.rect(0, 0, 0, 0);
@ -920,23 +1092,17 @@ Item {
const cursor = KWinComponents.Workspace.cursorPos; const cursor = KWinComponents.Workspace.cursorPos;
const target = findTileAtCursor(cursor, win); const target = findTileAtCursor(cursor, win);
if (target) { if (target && target.screen === dragSourceScreen && validRect(target.rect)) {
const position = tileInsertDirection(cursor, target.rect); dragSwapTarget = {
const insertRect = previewInsertRect(win, target, position); screen: target.screen,
if (validRect(insertRect)) { key: target.key,
dragInsertTarget = { rect: target.rect
screen: target.screen, };
key: target.key, showDragOutline("swap", target.rect);
direction: position, return;
rect: insertRect,
position: position
};
showDragOutline("insert", insertRect);
return;
}
} }
dragInsertTarget = null; dragSwapTarget = null;
if (outsideWorkArea(win, cursor)) { if (outsideWorkArea(win, cursor)) {
showDragOutline("float", validRect(geo) ? geo : win.frameGeometry); showDragOutline("float", validRect(geo) ? geo : win.frameGeometry);
} else { } else {
@ -948,10 +1114,10 @@ Item {
if (!isConvergence()) return; if (!isConvergence()) return;
draggingWindow = win; draggingWindow = win;
clearDragOutline(); clearDragOutline();
dragInsertTarget = null; dragSwapTarget = null;
dragSourceRect = Qt.rect(0, 0, 0, 0); dragSourceRect = Qt.rect(0, 0, 0, 0);
// Remember the source slot so we can insert on drop. // Remember the source slot so we can swap on drop.
// The tile stays in screenTiles[] during the drag so the rest of // The tile stays in screenTiles[] during the drag so the rest of
// the layout doesn't shuffle. // the layout doesn't shuffle.
const slot = findTileSlot(win); const slot = findTileSlot(win);
@ -993,8 +1159,8 @@ Item {
floatWindow(win); floatWindow(win);
} }
else if (finalDropMode === "insert" && dragInsertTarget) { else if (finalDropMode === "swap" && dragSwapTarget) {
insertDraggedWindow(dragInsertTarget); swapDraggedWindow(dragSwapTarget);
} }
// Dropped elsewhere restore the source tile to its original slot. // Dropped elsewhere restore the source tile to its original slot.
@ -1082,6 +1248,13 @@ Item {
} }
} }
KWinComponents.ShortcutHandler {
name: "SHIFT Tiling Cycle Layout"
text: "SHIFT Tiling: Cycle layout mode"
sequence: "Meta+Shift+T"
onActivated: root.cycleLayoutMode(root.outputNameForActiveWindow())
}
// Tiling on/off // Tiling on/off
KWinComponents.ShortcutHandler { KWinComponents.ShortcutHandler {
name: "SHIFT Tiling Toggle" name: "SHIFT Tiling Toggle"
@ -1103,5 +1276,6 @@ Item {
addWindow(win); addWindow(win);
} }
} }
rememberFocusedWindow(KWinComponents.Workspace.activeWindow);
} }
} }

View file

@ -42,6 +42,7 @@ require_line "$env_config" '"shift-tile-preview"'
require_line "$env_config" '"shift-tiling"' require_line "$env_config" '"shift-tiling"'
require_line "$effect_qml" "KWinComponents.SceneEffect" require_line "$effect_qml" "KWinComponents.SceneEffect"
require_line "$effect_qml" "visible: false"
require_line "$effect_qml" "ShellSettings.Settings.dynamicTilingEnabled" require_line "$effect_qml" "ShellSettings.Settings.dynamicTilingEnabled"
require_line "$effect_qml" "interactiveMoveResizeStarted.connect" require_line "$effect_qml" "interactiveMoveResizeStarted.connect"
require_line "$effect_qml" "interactiveMoveResizeStepped.connect" require_line "$effect_qml" "interactiveMoveResizeStepped.connect"
@ -50,31 +51,68 @@ require_line "$effect_qml" "Behavior on x"
require_line "$effect_qml" "Behavior on y" require_line "$effect_qml" "Behavior on y"
require_line "$effect_qml" "Behavior on width" require_line "$effect_qml" "Behavior on width"
require_line "$effect_qml" "Behavior on height" require_line "$effect_qml" "Behavior on height"
require_line "$effect_qml" "showPreview(\"insert\"" require_line "$effect_qml" "showPreview(\"swap\""
require_line "$effect_qml" "showPreview(\"float\"" require_line "$effect_qml" "showPreview(\"float\""
require_line "$effect_qml" "showPreview(\"restore\"" require_line "$effect_qml" "showPreview(\"restore\""
require_line "$effect_qml" "readonly property real insertIntentDeadZone: 0.18"
require_line "$effect_qml" "if (Math.abs(fromCenterX) < insertIntentDeadZone && Math.abs(fromCenterY) < insertIntentDeadZone)"
require_line "$tiling_script" "readonly property int maxWindowsPerPage: 4"
require_line "$tiling_script" "readonly property real stablePrimaryRatio: 0.58" require_line "$tiling_script" "readonly property real stablePrimaryRatio: 0.58"
require_line "$tiling_script" "readonly property real insertIntentDeadZone: 0.18" require_line "$tiling_script" "readonly property var layoutModes: [\"master\", \"columns\", \"rows\"]"
require_line "$tiling_script" "function desktopKey(desktop)"
require_line "$tiling_script" "function desktopForWindow(win)"
require_line "$tiling_script" "function layoutKeyFor(outputName, desktop)"
require_line "$tiling_script" "function desktopForLayoutKey(outputName)"
require_line "$tiling_script" "function layoutKeyForWindow(win)"
require_line "$tiling_script" "function assignWindowToDesktop(win, desktop)"
require_line "$tiling_script" "function targetLayoutForNewWindow(win)"
require_line "$tiling_script" "function leafIndexForWindow(outputName, win)"
require_line "$tiling_script" "function leafForSlot(outputName, preferredIndex)"
require_line "$tiling_script" "property var lastFocusedWindowKeys: ({})"
require_line "$tiling_script" "function rememberFocusedWindow(win)"
require_line "$tiling_script" "function replacementLeafForLayout(outputName, fallbackIndex)"
require_line "$tiling_script" "function swapWindowBetweenLayouts(win, sourceName, targetName)"
require_line "$tiling_script" "windowCountForLayout(currentKey) < maxWindowsPerPage"
require_line "$tiling_script" "windowCountForLayout(targetName) >= maxWindowsPerPage && swapWindowBetweenLayouts(win, currentName, targetName)"
require_line "$tiling_script" "KWinComponents.Workspace.currentDesktop = target.desktop"
require_line "$tiling_script" "assignWindowToDesktop(displacedWindow, sourceDesktop)"
require_line "$tiling_script" "const targetLeaf = replacementLeafForLayout(targetName, sourceIndex)"
require_line "$tiling_script" "function onActiveWindowChanged()"
require_line "$tiling_script" "root.rememberFocusedWindow(KWinComponents.Workspace.activeWindow)"
require_line "$tiling_script" "win.desktops = [desktop]"
require_line "$tiling_script" "win.desktopsChanged.connect"
require_line "$tiling_script" "function orderedWindowsForScreen(outputName)" require_line "$tiling_script" "function orderedWindowsForScreen(outputName)"
require_line "$tiling_script" "function buildStableStack(windows, startIndex)" require_line "$tiling_script" "function buildStableStack(windows, startIndex)"
require_line "$tiling_script" "function buildStableLayout(windows)" require_line "$tiling_script" "function buildLinearLayout(windows, startIndex, orientation)"
require_line "$tiling_script" "function layoutModeForScreen(outputName)"
require_line "$tiling_script" "function buildStableLayout(windows, mode)"
require_line "$tiling_script" "function setStableLayout(outputName, windows)" require_line "$tiling_script" "function setStableLayout(outputName, windows)"
require_line "$tiling_script" "function cycleLayoutMode(outputName)"
require_line "$tiling_script" "function swapDraggedWindow(target)"
require_line "$tiling_script" "showDragOutline(\"swap\", target.rect)"
require_line "$tiling_script" "swapDraggedWindow(dragSwapTarget)"
require_line "$tiling_script" "setStableLayout(outputName, windows)" require_line "$tiling_script" "setStableLayout(outputName, windows)"
require_line "$tiling_script" "setStableLayout(outputName, remaining)" require_line "$tiling_script" "setStableLayout(outputName, remaining)"
require_line "$tiling_script" "retileScreen(name)" require_line "$tiling_script" "retileScreen(name)"
require_line "$tiling_script" "retileScreen(targetName)" require_line "$tiling_script" "retileScreen(targetName)"
require_line "$tiling_script" "if (Math.abs(fromCenterX) < insertIntentDeadZone && Math.abs(fromCenterY) < insertIntentDeadZone)"
require_line "$tiling_script" "function promoteWindow(win)" require_line "$tiling_script" "function promoteWindow(win)"
require_line "$tiling_script" "setStableLayout(outputName, reordered)" require_line "$tiling_script" "setStableLayout(outputName, reordered)"
require_line "$tiling_script" "name: \"SHIFT Tiling Promote Primary\"" require_line "$tiling_script" "name: \"SHIFT Tiling Promote Primary\""
require_line "$tiling_script" "sequence: \"Meta+Shift+Return\"" require_line "$tiling_script" "sequence: \"Meta+Shift+Return\""
require_line "$tiling_script" "name: \"SHIFT Tiling Cycle Layout\""
require_line "$tiling_script" "sequence: \"Meta+Shift+T\""
reject_line "$tiling_script" "targetKey = lastLeafKey(rootNode)" reject_line "$tiling_script" "targetKey = lastLeafKey(rootNode)"
reject_line "$tiling_script" "function lastLeafKey(node)"
reject_line "$tiling_script" "win.output.name !== outputName"
reject_line "$tiling_script" "const targetName = output.name"
reject_line "$tiling_script" "const desktop = win.desktops[0]"
reject_line "$tiling_script" "setScreenLayout(outputName, splitLeaf(rootNode, targetKey" reject_line "$tiling_script" "setScreenLayout(outputName, splitLeaf(rootNode, targetKey"
reject_line "$tiling_script" "function moveWindowToSplit(target)"
reject_line "$tiling_script" "function splitLeaf(node, targetKey"
reject_line "$tiling_script" "function tileInsertDirection(cursor, rect)"
reject_line "$tiling_script" "showDragOutline(\"insert\""
reject_line "$tiling_script" "KWinComponents.Workspace.showOutline(dragOutlineRect)" reject_line "$tiling_script" "KWinComponents.Workspace.showOutline(dragOutlineRect)"
reject_line "$tiling_script" "KWinComponents.Workspace.hideOutline()" reject_line "$tiling_script" "KWinComponents.Workspace.hideOutline()"
reject_line "$effect_qml" "effect.visible = true"
printf '%s\n' 'dynamic-tiles-motion-ok' printf '%s\n' 'dynamic-tiles-motion-ok'