mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 00:47:22 +00:00
Refine Dynamic Tiling drag zones
Use edge insert zones for drag-and-drop reordering, keep drag signal connections stable across window creation, and consume task-menu tiling requests through the script settings bridge.
This commit is contained in:
parent
3f6916cafc
commit
3fbf68d56b
1 changed files with 293 additions and 70 deletions
|
|
@ -23,6 +23,7 @@ Item {
|
|||
// ── Configuration ───────────────────────────────────────────────────────
|
||||
readonly property int outerGap: 8
|
||||
readonly property int innerGap: 8 // half applied to each edge → 4px per tile
|
||||
readonly property int insertZoneThickness: 48
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -33,24 +34,31 @@ Item {
|
|||
// Windows the user has manually floated (by UUID string).
|
||||
property var floatedWindows: ({})
|
||||
|
||||
// Whether tiling is globally enabled.
|
||||
property bool tilingEnabled: true
|
||||
// Windows whose interactive move/resize signals are already connected.
|
||||
property var dragConnectedWindows: ({})
|
||||
|
||||
property int lastWindowRequestSerial: ShellSettings.Settings.dynamicTilingWindowRequestSerial
|
||||
|
||||
// Drag state.
|
||||
//
|
||||
// Behaviour: dragging a tile and dropping it onto ANOTHER tile swaps
|
||||
// their positions in the BSP layout. Dropping anywhere else does
|
||||
// nothing (the window will be re-tiled into its original slot on the
|
||||
// next layout pass, unless KWin's native quick-tile / electric border
|
||||
// takes over — which is fine; we don't fight it).
|
||||
// Behaviour: dragging a tile and dropping it onto ANOTHER tile swaps or
|
||||
// inserts based on the cursor zone. 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 var swapOutlineActive: false
|
||||
property bool dragOutlineActive: false
|
||||
property string dragDropMode: "" // "swap", "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 dragSwapTarget: null // {screen, index, rect} of tile under cursor
|
||||
property var dragInsertTarget: null // {screen, index, insertIndex, rect} preview slot
|
||||
readonly property int floatEscapeMargin: 32
|
||||
|
||||
// Deferred retile queue.
|
||||
// The dockSpaceReserver LayerShell exclusive zone needs one Wayland
|
||||
|
|
@ -118,12 +126,11 @@ Item {
|
|||
if (minW > 0 && maxW > 0 && minW >= maxW) return true;
|
||||
if (minH > 0 && maxH > 0 && minH >= maxH) return true;
|
||||
// Manually floated
|
||||
if (floatedWindows[win.internalId]) return true;
|
||||
if (floatedWindows[windowKey(win)]) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isTileable(win) {
|
||||
if (!tilingEnabled) return false;
|
||||
if (!isConvergence()) return false;
|
||||
if (shouldIgnore(win)) return false;
|
||||
if (shouldFloat(win)) return false;
|
||||
|
|
@ -240,6 +247,8 @@ Item {
|
|||
function addWindow(win) {
|
||||
if (!isTileable(win)) return;
|
||||
|
||||
connectDragHandlers(win);
|
||||
|
||||
const output = win.output;
|
||||
if (!output) return;
|
||||
const name = output.name;
|
||||
|
|
@ -251,7 +260,7 @@ Item {
|
|||
// Avoid duplicates
|
||||
const tiles = screenTiles[name];
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
if (tiles[i].win.internalId === win.internalId) return;
|
||||
if (windowKey(tiles[i].win) === windowKey(win)) return;
|
||||
}
|
||||
|
||||
tiles.push({ win: win, rect: Qt.rect(0, 0, 0, 0) });
|
||||
|
|
@ -275,7 +284,7 @@ Item {
|
|||
for (const sName in screenTiles) {
|
||||
const tiles = screenTiles[sName];
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
if (tiles[i].win.internalId === win.internalId) {
|
||||
if (windowKey(tiles[i].win) === windowKey(win)) {
|
||||
tiles.splice(i, 1);
|
||||
screenTiles[sName] = tiles;
|
||||
retileScreen(sName);
|
||||
|
|
@ -285,6 +294,64 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -306,7 +373,7 @@ Item {
|
|||
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const t = tiles[i];
|
||||
if (t.win.internalId === fromWin.internalId) continue;
|
||||
if (windowKey(t.win) === windowKey(fromWin)) continue;
|
||||
const tc = centreOf(t.rect);
|
||||
const dx = tc.x - fc.x;
|
||||
const dy = tc.y - fc.y;
|
||||
|
|
@ -336,13 +403,12 @@ Item {
|
|||
function onWindowAdded(win) {
|
||||
if (isTileable(win)) {
|
||||
addWindow(win);
|
||||
win.interactiveMoveResizeStarted.connect(function() { root.onDragStart(win); });
|
||||
win.interactiveMoveResizeStepped.connect(function(geo) { root.onDragStep(win, geo); });
|
||||
win.interactiveMoveResizeFinished.connect(function() { root.onDragEnd(win); });
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowRemoved(win) {
|
||||
const key = root.windowKey(win);
|
||||
if (key) delete root.dragConnectedWindows[key];
|
||||
root.removeWindow(win);
|
||||
}
|
||||
}
|
||||
|
|
@ -385,6 +451,10 @@ Item {
|
|||
screenTiles = {};
|
||||
}
|
||||
}
|
||||
|
||||
function onDynamicTilingWindowRequestChanged() {
|
||||
root.handleWindowTilingRequest();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Drag handlers ─────────────────────────────────────────────────────
|
||||
|
|
@ -394,7 +464,7 @@ Item {
|
|||
for (const sName in screenTiles) {
|
||||
const tiles = screenTiles[sName];
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
if (tiles[i].win && tiles[i].win.internalId === win.internalId) {
|
||||
if (tiles[i].win && windowKey(tiles[i].win) === windowKey(win)) {
|
||||
return { screen: sName, index: i };
|
||||
}
|
||||
}
|
||||
|
|
@ -408,7 +478,7 @@ Item {
|
|||
const tiles = screenTiles[sName];
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const t = tiles[i];
|
||||
if (ignoreWin && t.win && t.win.internalId === ignoreWin.internalId) continue;
|
||||
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 &&
|
||||
|
|
@ -420,60 +490,230 @@ Item {
|
|||
return null;
|
||||
}
|
||||
|
||||
function tileInsertPosition(cursor, rect) {
|
||||
if (!validRect(rect)) return "";
|
||||
|
||||
const zone = Math.min(insertZoneThickness, Math.max(16, Math.floor(Math.min(rect.width, rect.height) / 4)));
|
||||
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 > zone) return "";
|
||||
if (nearest === left || nearest === top) return "before";
|
||||
return "after";
|
||||
}
|
||||
|
||||
function insertIndexForTarget(target, position) {
|
||||
if (!target) return -1;
|
||||
|
||||
let targetIndex = target.index;
|
||||
if (dragSourceScreen === target.screen && dragSourceIndex >= 0 && dragSourceIndex < targetIndex) {
|
||||
targetIndex--;
|
||||
}
|
||||
return position === "before" ? targetIndex : targetIndex + 1;
|
||||
}
|
||||
|
||||
function previewInsertRect(win, target, position) {
|
||||
if (!target || !target.rect || dragSourceIndex < 0) return null;
|
||||
|
||||
const tiles = screenTiles[target.screen];
|
||||
if (!tiles || !tiles[target.index]) return null;
|
||||
|
||||
const area = workRect(tiles[target.index].win || win);
|
||||
if (!area) return null;
|
||||
|
||||
const count = tiles.length + (dragSourceScreen === target.screen ? 0 : 1);
|
||||
const insertIndex = insertIndexForTarget(target, position);
|
||||
if (insertIndex < 0 || insertIndex >= count) return null;
|
||||
|
||||
const rawRects = bspRects(area, count);
|
||||
const gappedRects = applyGaps(rawRects, area);
|
||||
return gappedRects[insertIndex] || null;
|
||||
}
|
||||
|
||||
function insertDraggedWindow(target) {
|
||||
if (!target || dragSourceIndex < 0 || !dragSourceScreen) return;
|
||||
|
||||
if (dragSourceScreen === target.screen) {
|
||||
const tiles = screenTiles[dragSourceScreen] ? screenTiles[dragSourceScreen].slice() : null;
|
||||
if (!tiles || !tiles[dragSourceIndex]) return;
|
||||
|
||||
const node = tiles.splice(dragSourceIndex, 1)[0];
|
||||
const insertIndex = Math.max(0, Math.min(target.insertIndex, tiles.length));
|
||||
tiles.splice(insertIndex, 0, node);
|
||||
screenTiles[dragSourceScreen] = tiles;
|
||||
retileScreen(dragSourceScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceTiles = screenTiles[dragSourceScreen] ? screenTiles[dragSourceScreen].slice() : null;
|
||||
const targetTiles = screenTiles[target.screen] ? screenTiles[target.screen].slice() : null;
|
||||
if (!sourceTiles || !targetTiles || !sourceTiles[dragSourceIndex]) return;
|
||||
|
||||
const node = sourceTiles.splice(dragSourceIndex, 1)[0];
|
||||
const insertIndex = Math.max(0, Math.min(target.insertIndex, targetTiles.length));
|
||||
targetTiles.splice(insertIndex, 0, node);
|
||||
screenTiles[dragSourceScreen] = sourceTiles;
|
||||
screenTiles[target.screen] = targetTiles;
|
||||
retileScreen(dragSourceScreen);
|
||||
retileScreen(target.screen);
|
||||
}
|
||||
|
||||
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);
|
||||
KWinComponents.Workspace.showOutline(dragOutlineRect);
|
||||
dragOutlineActive = true;
|
||||
}
|
||||
|
||||
function clearDragOutline() {
|
||||
if (dragOutlineActive) {
|
||||
KWinComponents.Workspace.hideOutline();
|
||||
}
|
||||
dragOutlineActive = false;
|
||||
dragDropMode = "";
|
||||
dragOutlineRect = Qt.rect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
function resetDragState() {
|
||||
clearDragOutline();
|
||||
dragSwapTarget = null;
|
||||
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 = tileInsertPosition(cursor, target.rect);
|
||||
const insertRect = position !== "" ? previewInsertRect(win, target, position) : null;
|
||||
if (validRect(insertRect)) {
|
||||
dragSwapTarget = null;
|
||||
dragInsertTarget = {
|
||||
screen: target.screen,
|
||||
index: target.index,
|
||||
insertIndex: insertIndexForTarget(target, position),
|
||||
rect: insertRect,
|
||||
position: position
|
||||
};
|
||||
showDragOutline("insert", insertRect);
|
||||
return;
|
||||
}
|
||||
|
||||
dragSwapTarget = target;
|
||||
dragInsertTarget = null;
|
||||
showDragOutline("swap", target.rect);
|
||||
return;
|
||||
}
|
||||
|
||||
dragSwapTarget = null;
|
||||
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;
|
||||
swapOutlineActive = false;
|
||||
clearDragOutline();
|
||||
dragSwapTarget = null;
|
||||
dragInsertTarget = null;
|
||||
dragSourceRect = Qt.rect(0, 0, 0, 0);
|
||||
|
||||
// Remember the source slot so we can swap on drop.
|
||||
// Remember the source slot so we can swap or 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()) return;
|
||||
if (draggingWindow !== win) return;
|
||||
|
||||
// Only show an outline when the cursor is over another tile —
|
||||
// a clear visual hint that "drop here = swap".
|
||||
const cursor = KWinComponents.Workspace.cursorPos;
|
||||
const target = findTileAtCursor(cursor, win);
|
||||
|
||||
if (target) {
|
||||
if (!dragSwapTarget ||
|
||||
dragSwapTarget.screen !== target.screen ||
|
||||
dragSwapTarget.index !== target.index) {
|
||||
dragSwapTarget = target;
|
||||
KWinComponents.Workspace.showOutline(target.rect);
|
||||
swapOutlineActive = true;
|
||||
}
|
||||
} else {
|
||||
dragSwapTarget = null;
|
||||
if (swapOutlineActive) {
|
||||
KWinComponents.Workspace.hideOutline();
|
||||
swapOutlineActive = false;
|
||||
}
|
||||
if (!isConvergence()) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
if (draggingWindow !== win) return;
|
||||
updateDragIntent(win, geo);
|
||||
}
|
||||
|
||||
function onDragEnd(win) {
|
||||
if (!isConvergence()) return;
|
||||
if (swapOutlineActive) {
|
||||
KWinComponents.Workspace.hideOutline();
|
||||
swapOutlineActive = false;
|
||||
if (!isConvergence()) {
|
||||
resetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
const finalDropMode = dragDropMode;
|
||||
clearDragOutline();
|
||||
|
||||
if (finalDropMode === "float") {
|
||||
floatWindow(win);
|
||||
}
|
||||
|
||||
else if (finalDropMode === "insert" && dragInsertTarget) {
|
||||
insertDraggedWindow(dragInsertTarget);
|
||||
}
|
||||
|
||||
// Dropped on another tile → swap source/target slots.
|
||||
if (dragSwapTarget && dragSourceScreen && dragSourceIndex >= 0) {
|
||||
else if (dragSwapTarget && dragSourceScreen && dragSourceIndex >= 0) {
|
||||
const sScreen = dragSourceScreen;
|
||||
const sIdx = dragSourceIndex;
|
||||
const tScreen = dragSwapTarget.screen;
|
||||
|
|
@ -506,10 +746,7 @@ Item {
|
|||
retileScreen(dragSourceScreen);
|
||||
}
|
||||
|
||||
dragSwapTarget = null;
|
||||
dragSourceScreen = "";
|
||||
dragSourceIndex = -1;
|
||||
draggingWindow = null;
|
||||
resetDragState();
|
||||
}
|
||||
|
||||
// ── Keyboard shortcuts ─────────────────────────────────────────────────
|
||||
|
|
@ -568,13 +805,11 @@ Item {
|
|||
onActivated: {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win) return;
|
||||
const id = win.internalId;
|
||||
const id = root.windowKey(win);
|
||||
if (root.floatedWindows[id]) {
|
||||
delete root.floatedWindows[id];
|
||||
root.addWindow(win);
|
||||
root.tileWindow(win);
|
||||
} else {
|
||||
root.floatedWindows[id] = true;
|
||||
root.removeWindow(win);
|
||||
root.floatWindow(win);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -585,21 +820,12 @@ Item {
|
|||
text: "SHIFT Tiling: Toggle tiling on/off"
|
||||
sequence: "Meta+T"
|
||||
onActivated: {
|
||||
root.tilingEnabled = !root.tilingEnabled;
|
||||
if (root.tilingEnabled) {
|
||||
const wins = KWinComponents.Workspace.windows;
|
||||
for (let i = 0; i < wins.length; i++) root.addWindow(wins[i]);
|
||||
} else {
|
||||
root.screenTiles = {};
|
||||
}
|
||||
ShellSettings.Settings.dynamicTilingEnabled = !ShellSettings.Settings.dynamicTilingEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Right-click menu ──────────────────────────────────────────────────
|
||||
// ── Component setup ───────────────────────────────────────────────────
|
||||
|
||||
// Note: registerUserActionsMenu is a global function in KWin JS scripts.
|
||||
// In declarative QML scripts it is exposed via the KWin global object.
|
||||
// We wire it up after the component is complete.
|
||||
Component.onCompleted: {
|
||||
// Connect to existing windows
|
||||
const wins = KWinComponents.Workspace.windows;
|
||||
|
|
@ -607,9 +833,6 @@ Item {
|
|||
const win = wins[i];
|
||||
if (isTileable(win)) {
|
||||
addWindow(win);
|
||||
win.interactiveMoveResizeStarted.connect(function() { root.onDragStart(win); });
|
||||
win.interactiveMoveResizeStepped.connect(function(geo) { root.onDragStep(win, geo); });
|
||||
win.interactiveMoveResizeFinished.connect(function() { root.onDragEnd(win); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue