mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 16:57:43 +00:00
Nested KWin blacks out when this scene effect becomes visible. Leave the effect disabled and let the tiling script own swap and restore outlines.
330 lines
No EOL
10 KiB
QML
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()
|
|
} |