shift-shell/kwin/effects/shift-tile-preview/contents/ui/main.qml
Marco Allegretti 40beabede9 Keep tile preview effect inert
Nested KWin blacks out when this scene effect becomes visible. Leave the effect disabled and let the tiling script own swap and restore outlines.
2026-05-20 09:38:22 +02:00

330 lines
No EOL
10 KiB
QML

// 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
visible: false
readonly property int outerGap: 8
readonly property int floatEscapeMargin: 32
readonly property int previewAnimationDuration: 180
readonly property int previewFadeDuration: 110
property var dragConnectedWindows: ({})
property var draggingWindow: null
property rect dragSourceGeometry: Qt.rect(0, 0, 0, 0)
property bool animatePreview: false
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) {
hidePreview();
}
function hidePreview() {
previewVisible = false;
draggingWindow = null;
dragSourceGeometry = Qt.rect(0, 0, 0, 0);
disableEffectTimer.restart();
}
function updatePreview(window, dragGeometry) {
if (!isActive() || draggingWindow !== window || !window.output) {
hidePreview();
return;
}
const cursor = KWinComponents.Workspace.cursorPos;
const targetGeometry = findTileAtCursor(cursor, window);
if (targetGeometry) {
showPreview("swap", targetGeometry, window.output.name);
return;
}
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]);
}
}
function previewFillColor(mode) {
if (mode === "float") {
return Qt.rgba(1.0, 0.62, 0.24, 0.18);
}
if (mode === "restore") {
return Qt.rgba(1.0, 1.0, 1.0, 0.10);
}
return Qt.rgba(0.18, 0.72, 0.66, 0.22);
}
function previewBorderColor(mode) {
if (mode === "float") {
return Qt.rgba(1.0, 0.72, 0.36, 0.72);
}
if (mode === "restore") {
return Qt.rgba(1.0, 1.0, 1.0, 0.36);
}
return Qt.rgba(0.64, 0.90, 0.86, 0.82);
}
Timer {
id: enableAnimationTimer
interval: 1
repeat: false
onTriggered: effect.animatePreview = true
}
Timer {
id: disableEffectTimer
interval: effect.previewFadeDuration
repeat: false
onTriggered: {
if (!effect.previewVisible) {
effect.visible = false;
}
}
}
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();
}
}
}
delegate: Rectangle {
id: screenDelegate
readonly property var targetScreen: KWinComponents.SceneView.screen
readonly property bool previewOnScreen: effect.previewScreenName === targetScreen.name
color: "transparent"
Rectangle {
id: previewSurface
visible: opacity > 0
x: effect.previewGeometry.x - screenDelegate.targetScreen.geometry.x
y: effect.previewGeometry.y - screenDelegate.targetScreen.geometry.y
width: effect.previewGeometry.width
height: effect.previewGeometry.height
radius: 14
opacity: effect.previewVisible && screenDelegate.previewOnScreen ? 1 : 0
color: effect.previewFillColor(effect.previewMode)
border.width: 2
border.color: effect.previewBorderColor(effect.previewMode)
Behavior on x {
enabled: effect.animatePreview
NumberAnimation { duration: effect.previewAnimationDuration; easing.type: Easing.OutCubic }
}
Behavior on y {
enabled: effect.animatePreview
NumberAnimation { duration: effect.previewAnimationDuration; easing.type: Easing.OutCubic }
}
Behavior on width {
enabled: effect.animatePreview
NumberAnimation { duration: effect.previewAnimationDuration; easing.type: Easing.OutCubic }
}
Behavior on height {
enabled: effect.animatePreview
NumberAnimation { duration: effect.previewAnimationDuration; easing.type: Easing.OutCubic }
}
Behavior on opacity {
NumberAnimation { duration: effect.previewFadeDuration; easing.type: Easing.InOutQuad }
}
Behavior on color {
ColorAnimation { duration: effect.previewFadeDuration; easing.type: Easing.OutCubic }
}
Behavior on border.color {
ColorAnimation { duration: effect.previewFadeDuration; easing.type: Easing.OutCubic }
}
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: Math.max(0, parent.radius - anchors.margins)
color: "transparent"
border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.14)
opacity: effect.previewMode === "insert" ? 1 : 0.45
}
}
}
Component.onCompleted: connectExistingWindows()
}