2026-05-19 07:46:34 +00:00
|
|
|
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
|
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
|
|
|
|
|
|
import QtQuick
|
|
|
|
|
import org.kde.kwin as KWinComponents
|
|
|
|
|
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
|
|
|
|
|
|
|
|
|
KWinComponents.SceneEffect {
|
|
|
|
|
id: effect
|
|
|
|
|
|
2026-05-20 07:38:22 +00:00
|
|
|
visible: false
|
2026-05-19 07:46:34 +00:00
|
|
|
|
|
|
|
|
readonly property int outerGap: 8
|
|
|
|
|
readonly property int floatEscapeMargin: 32
|
|
|
|
|
|
|
|
|
|
property var dragConnectedWindows: ({})
|
|
|
|
|
property var draggingWindow: null
|
|
|
|
|
property rect dragSourceGeometry: Qt.rect(0, 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
property bool previewVisible: false
|
|
|
|
|
property string previewMode: ""
|
|
|
|
|
property string previewScreenName: ""
|
|
|
|
|
property rect previewGeometry: Qt.rect(0, 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
function isActive() {
|
|
|
|
|
return ShellSettings.Settings.convergenceModeEnabled
|
|
|
|
|
&& !ShellSettings.Settings.gamingModeEnabled
|
|
|
|
|
&& ShellSettings.Settings.dynamicTilingEnabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function windowKey(window) {
|
|
|
|
|
return window && window.internalId !== undefined ? String(window.internalId) : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shouldIgnore(window) {
|
|
|
|
|
if (!window || window.deleted) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (!window.normalWindow || !window.maximizable || window.fullScreen) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return window.resourceClass === "xwaylandvideobridge";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validRect(geometry) {
|
|
|
|
|
return geometry && geometry.width > 0 && geometry.height > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rectContainsPoint(geometry, point) {
|
|
|
|
|
return validRect(geometry)
|
|
|
|
|
&& point.x >= geometry.x
|
|
|
|
|
&& point.x <= geometry.x + geometry.width
|
|
|
|
|
&& point.y >= geometry.y
|
|
|
|
|
&& point.y <= geometry.y + geometry.height;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function workRect(window) {
|
|
|
|
|
if (!window || !window.output || window.desktops.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return KWinComponents.Workspace.clientArea(
|
|
|
|
|
KWinComponents.Workspace.MaximizeArea,
|
|
|
|
|
window.output,
|
|
|
|
|
window.desktops[0]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function outsideWorkArea(window, cursor) {
|
|
|
|
|
const area = workRect(window);
|
|
|
|
|
if (!area) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const expandedArea = Qt.rect(
|
|
|
|
|
area.x - floatEscapeMargin,
|
|
|
|
|
area.y - floatEscapeMargin,
|
|
|
|
|
area.width + floatEscapeMargin * 2,
|
|
|
|
|
area.height + floatEscapeMargin * 2
|
|
|
|
|
);
|
|
|
|
|
return !rectContainsPoint(expandedArea, cursor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findTileAtCursor(cursor, ignoredWindow) {
|
|
|
|
|
const ignoredKey = windowKey(ignoredWindow);
|
|
|
|
|
const windows = KWinComponents.Workspace.windows;
|
|
|
|
|
let bestWindow = null;
|
|
|
|
|
let bestGeometry = Qt.rect(0, 0, 0, 0);
|
|
|
|
|
let bestArea = 0;
|
|
|
|
|
|
|
|
|
|
for (let index = 0; index < windows.length; index++) {
|
|
|
|
|
const candidate = windows[index];
|
|
|
|
|
if (shouldIgnore(candidate) || windowKey(candidate) === ignoredKey) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!ignoredWindow || !candidate.output || !ignoredWindow.output || candidate.output.name !== ignoredWindow.output.name) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const geometry = candidate.frameGeometry;
|
|
|
|
|
if (!rectContainsPoint(geometry, cursor)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const area = geometry.width * geometry.height;
|
|
|
|
|
if (!bestWindow || area < bestArea) {
|
|
|
|
|
bestWindow = candidate;
|
|
|
|
|
bestGeometry = geometry;
|
|
|
|
|
bestArea = area;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bestWindow ? bestGeometry : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function insetPreviewGeometry(geometry) {
|
|
|
|
|
const inset = Math.max(2, Math.round(outerGap / 2));
|
|
|
|
|
return Qt.rect(
|
|
|
|
|
geometry.x + inset,
|
|
|
|
|
geometry.y + inset,
|
|
|
|
|
Math.max(1, geometry.width - inset * 2),
|
|
|
|
|
Math.max(1, geometry.height - inset * 2)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showPreview(mode, geometry, screenName) {
|
2026-05-27 19:33:34 +00:00
|
|
|
if (!isActive() || !validRect(geometry)) {
|
|
|
|
|
hidePreview();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
previewMode = mode;
|
|
|
|
|
previewScreenName = screenName;
|
|
|
|
|
previewGeometry = insetPreviewGeometry(geometry);
|
|
|
|
|
previewVisible = true;
|
|
|
|
|
KWinComponents.Workspace.showOutline(previewGeometry);
|
2026-05-19 07:46:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hidePreview() {
|
|
|
|
|
previewVisible = false;
|
|
|
|
|
draggingWindow = null;
|
|
|
|
|
dragSourceGeometry = Qt.rect(0, 0, 0, 0);
|
2026-05-27 19:33:34 +00:00
|
|
|
previewMode = "";
|
|
|
|
|
previewScreenName = "";
|
|
|
|
|
previewGeometry = Qt.rect(0, 0, 0, 0);
|
|
|
|
|
KWinComponents.Workspace.hideOutline();
|
2026-05-19 07:46:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updatePreview(window, dragGeometry) {
|
|
|
|
|
if (!isActive() || draggingWindow !== window || !window.output) {
|
|
|
|
|
hidePreview();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cursor = KWinComponents.Workspace.cursorPos;
|
|
|
|
|
const targetGeometry = findTileAtCursor(cursor, window);
|
|
|
|
|
if (targetGeometry) {
|
2026-05-20 07:38:22 +00:00
|
|
|
showPreview("swap", targetGeometry, window.output.name);
|
|
|
|
|
return;
|
2026-05-19 07:46:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (outsideWorkArea(window, cursor)) {
|
|
|
|
|
showPreview("float", validRect(dragGeometry) ? dragGeometry : window.frameGeometry, window.output.name);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showPreview("restore", dragSourceGeometry, window.output.name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function connectDragHandlers(window) {
|
|
|
|
|
const key = windowKey(window);
|
|
|
|
|
if (!key || dragConnectedWindows[key]) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dragConnectedWindows[key] = true;
|
|
|
|
|
window.interactiveMoveResizeStarted.connect(function() {
|
|
|
|
|
if (!isActive() || shouldIgnore(window)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
draggingWindow = window;
|
|
|
|
|
dragSourceGeometry = window.frameGeometry;
|
|
|
|
|
showPreview("restore", dragSourceGeometry, window.output ? window.output.name : "");
|
|
|
|
|
});
|
|
|
|
|
window.interactiveMoveResizeStepped.connect(function(geometry) {
|
|
|
|
|
updatePreview(window, geometry);
|
|
|
|
|
});
|
|
|
|
|
window.interactiveMoveResizeFinished.connect(function() {
|
|
|
|
|
if (draggingWindow === window) {
|
|
|
|
|
hidePreview();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function connectExistingWindows() {
|
|
|
|
|
const windows = KWinComponents.Workspace.windows;
|
|
|
|
|
for (let index = 0; index < windows.length; index++) {
|
|
|
|
|
connectDragHandlers(windows[index]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Connections {
|
|
|
|
|
target: KWinComponents.Workspace
|
|
|
|
|
|
|
|
|
|
function onWindowAdded(window) {
|
|
|
|
|
effect.connectDragHandlers(window);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onWindowRemoved(window) {
|
|
|
|
|
const key = effect.windowKey(window);
|
|
|
|
|
if (key) {
|
|
|
|
|
delete effect.dragConnectedWindows[key];
|
|
|
|
|
}
|
|
|
|
|
if (effect.draggingWindow === window) {
|
|
|
|
|
effect.hidePreview();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Connections {
|
|
|
|
|
target: ShellSettings.Settings
|
|
|
|
|
|
|
|
|
|
function onConvergenceModeEnabledChanged() {
|
|
|
|
|
if (!effect.isActive()) {
|
|
|
|
|
effect.hidePreview();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onGamingModeEnabledChanged() {
|
|
|
|
|
if (!effect.isActive()) {
|
|
|
|
|
effect.hidePreview();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onDynamicTilingEnabledChanged() {
|
|
|
|
|
if (!effect.isActive()) {
|
|
|
|
|
effect.hidePreview();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Component.onCompleted: connectExistingWindows()
|
|
|
|
|
}
|