diff --git a/kwin/decorations/org.shift.decoration/contents/ui/main.qml b/kwin/decorations/org.shift.decoration/contents/ui/main.qml index bf9561d8..1d3f3e09 100644 --- a/kwin/decorations/org.shift.decoration/contents/ui/main.qml +++ b/kwin/decorations/org.shift.decoration/contents/ui/main.qml @@ -181,10 +181,9 @@ Decoration { } } - // Snap-assist hover trigger lives in the shift-tiling KWin script: - // the decoration QML sandbox has no DBus / kglobalaccel access, so - // the script polls the cursor over the active window's titlebar - // and invokes the SHIFT Snap Assist shortcut after a short hover. + // Snap-layout hover activation lives in the SHIFT Snap Assist effect. + // The decoration only renders the maximize button; the effect decides + // when maximize-hover is eligible and shows the popup. Rectangle { visible: !isSpacer diff --git a/kwin/effects/shift-snap-assist/contents/ui/main.qml b/kwin/effects/shift-snap-assist/contents/ui/main.qml index 50b1c438..3c350568 100644 --- a/kwin/effects/shift-snap-assist/contents/ui/main.qml +++ b/kwin/effects/shift-snap-assist/contents/ui/main.qml @@ -6,19 +6,252 @@ // Presents a floating panel of layout preset buttons. // Activated by: // 1. Meta+Shift+S keyboard shortcut -// 2. The SHIFT decoration invoking the kglobalaccel shortcut on maximize-hover +// 2. Hovering the maximize button in convergence mode while dynamic tiling is off // // When a preset is clicked, the active window's frameGeometry is set to the -// chosen zone (with outer gaps applied). If a second zone is non-empty a -// "snap assist" strip shows recently-used windows as targets. +// chosen zone (with outer gaps applied). import QtQuick import QtQuick.Layouts import org.kde.kwin as KWinComponents +import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings KWinComponents.SceneEffect { id: effect + visible: false + + readonly property bool snapLayoutsEligible: ShellSettings.Settings.convergenceModeEnabled + && !ShellSettings.Settings.gamingModeEnabled + && !ShellSettings.Settings.dynamicTilingEnabled + readonly property int hoverBarHeight: 30 + readonly property int decorationButtonSize: 16 + readonly property int decorationButtonSpacing: 8 + readonly property int decorationButtonSideMargin: 12 + readonly property int maximizeButtonPadding: 4 + readonly property int hoverTimerInterval: 75 + readonly property int hoverDwellTicks: 14 + readonly property int hoverMoveTolerance: 5 + readonly property int hoverCooldownMs: 1200 + readonly property int panelDismissMargin: 24 + readonly property int panelScreenMargin: 8 + readonly property int panelCursorGap: 12 + readonly property int panelCursorRightBias: 34 + property var hoverWindowId: null + property int hoverTicks: 0 + property string hoverWindowStateKey: "" + property bool hoverSuppressedUntilLeave: false + property point hoverAnchorPos: Qt.point(0, 0) + property double hoverCooldownUntil: 0 + property var panelAnchorPos: Qt.point(0, 0) + property string panelAnchorScreenName: "" + property bool previewVisible: false + property rect previewGeometry: Qt.rect(0, 0, 0, 0) + property rect previewArea: Qt.rect(0, 0, 0, 0) + property var previewZones: [] + property int previewActiveIndex: -1 + property string previewScreenName: "" + + function resetHoverState() { + hoverWindowId = null; + hoverTicks = 0; + hoverWindowStateKey = ""; + hoverSuppressedUntilLeave = false; + hoverAnchorPos = Qt.point(0, 0); + } + + function resetHoverCandidate(win) { + hoverWindowId = win ? win.internalId : null; + hoverTicks = 1; + hoverWindowStateKey = win ? windowStateKey(win) : ""; + hoverSuppressedUntilLeave = false; + hoverAnchorPos = KWinComponents.Workspace.cursorPos; + } + + function setHoverCooldown() { + hoverCooldownUntil = Date.now() + hoverCooldownMs; + } + + function hoverOnCooldown() { + return Date.now() < hoverCooldownUntil; + } + + function hoverMovedTooFar() { + const cursor = KWinComponents.Workspace.cursorPos; + const dx = cursor.x - hoverAnchorPos.x; + const dy = cursor.y - hoverAnchorPos.y; + return dx * dx + dy * dy > hoverMoveTolerance * hoverMoveTolerance; + } + + function windowStateKey(win) { + const geometry = win.frameGeometry; + const maximized = win.maximized === undefined ? "" : win.maximized; + return maximized + ":" + geometry.x + "," + geometry.y + "," + geometry.width + "x" + geometry.height; + } + + function hideSnapLayouts() { + if (visible) { + setHoverCooldown(); + } + visible = false; + resetHoverState(); + panelAnchorScreenName = ""; + hideSnapPreview(); + } + + function showSnapLayouts() { + const win = KWinComponents.Workspace.activeWindow; + panelAnchorPos = KWinComponents.Workspace.cursorPos; + panelAnchorScreenName = win && win.output ? win.output.name : ""; + visible = true; + } + + function toggleActiveWindowMaximized() { + const win = KWinComponents.Workspace.activeWindow; + if (!win || !win.normalWindow || win.fullScreen || !win.maximizable) { + hideSnapLayouts(); + return; + } + + const maximize = !win.maximized; + win.setMaximize(maximize, maximize); + hideSnapLayouts(); + setHoverCooldown(); + } + + function hideSnapPreview() { + previewVisible = false; + previewScreenName = ""; + previewZones = []; + previewActiveIndex = -1; + KWinComponents.Workspace.hideOutline(); + } + + function showSnapPreview(preset, activeIndex) { + const win = KWinComponents.Workspace.activeWindow; + if (!win || !win.output) { + hideSnapPreview(); + return; + } + + const desktop = win.desktops.length > 0 ? win.desktops[0] : null; + if (!desktop) { + hideSnapPreview(); + return; + } + + const area = KWinComponents.Workspace.clientArea(KWinComponents.Workspace.MaximizeArea, win.output, desktop); + const zone = preset.zones[activeIndex]; + const gap = effect.outerGap; + previewGeometry = Qt.rect( + area.x + Math.round(zone.x * area.width) + gap, + area.y + Math.round(zone.y * area.height) + gap, + Math.round(zone.w * area.width) - 2 * gap, + Math.round(zone.h * area.height) - 2 * gap + ); + previewArea = area; + previewZones = preset.zones; + previewActiveIndex = activeIndex; + previewScreenName = win.output.name; + previewVisible = true; + KWinComponents.Workspace.hideOutline(); + } + + function cursorInActiveWindowMaximizeStrip() { + const win = KWinComponents.Workspace.activeWindow; + if (!win || !win.normalWindow || win.fullScreen || !win.maximizable) { + return false; + } + + const cursor = KWinComponents.Workspace.cursorPos; + const button = maximizeButtonRect(win); + if (button.width <= 0 || button.height <= 0) { + return false; + } + + return cursor.x >= button.x - effect.maximizeButtonPadding + && cursor.x <= button.x + button.width + effect.maximizeButtonPadding + && cursor.y >= button.y - effect.maximizeButtonPadding + && cursor.y <= button.y + button.height + effect.maximizeButtonPadding; + } + + function decorationButtonVisible(code) { + return code === "M" || code === "N" || code === "I" || code === "A" || code === "X"; + } + + function visibleDecorationButtons(sequence) { + const buttons = []; + for (let i = 0; i < sequence.length; i++) { + const code = sequence[i]; + if (decorationButtonVisible(code)) { + buttons.push(code); + } + } + return buttons; + } + + function maximizeButtonRect(win) { + const fg = win.frameGeometry; + const buttonY = fg.y + Math.round((effect.hoverBarHeight - effect.decorationButtonSize) / 2); + const leftButtons = visibleDecorationButtons(ShellSettings.KWinSettings.titleButtonsOnLeft); + const leftIndex = leftButtons.indexOf("A"); + if (leftIndex >= 0) { + return Qt.rect( + fg.x + effect.decorationButtonSideMargin + leftIndex * (effect.decorationButtonSize + effect.decorationButtonSpacing), + buttonY, + effect.decorationButtonSize, + effect.decorationButtonSize + ); + } + + const rightButtons = visibleDecorationButtons(ShellSettings.KWinSettings.titleButtonsOnRight); + const rightIndex = rightButtons.indexOf("A"); + if (rightIndex >= 0) { + const rowWidth = rightButtons.length * effect.decorationButtonSize + + Math.max(0, rightButtons.length - 1) * effect.decorationButtonSpacing; + return Qt.rect( + fg.x + fg.width - effect.decorationButtonSideMargin - rowWidth + + rightIndex * (effect.decorationButtonSize + effect.decorationButtonSpacing), + buttonY, + effect.decorationButtonSize, + effect.decorationButtonSize + ); + } + + return Qt.rect(0, 0, 0, 0); + } + + function cursorInPanel(screen) { + if (!screen) { + return false; + } + if (panelAnchorScreenName !== "" && panelAnchorScreenName !== screen.name) { + return false; + } + + const cursor = KWinComponents.Workspace.cursorPos; + const panel = panelRect(screen); + return cursor.x >= panel.x - panelDismissMargin + && cursor.x <= panel.x + panel.width + panelDismissMargin + && cursor.y >= panel.y - panelDismissMargin + && cursor.y <= panel.y + panel.height + panelDismissMargin; + } + + function panelRect(screen) { + const minX = screen.geometry.x + panelScreenMargin; + const maxX = screen.geometry.x + screen.geometry.width - snapPanelWidth - panelScreenMargin; + const minY = screen.geometry.y + panelScreenMargin; + const maxY = screen.geometry.y + screen.geometry.height - snapPanelHeight - panelScreenMargin; + const wantedX = panelAnchorPos.x - snapPanelWidth + panelCursorRightBias; + const wantedY = panelAnchorPos.y + panelCursorGap; + return Qt.rect( + Math.max(minX, Math.min(maxX, wantedX)), + Math.max(minY, Math.min(maxY, wantedY)), + snapPanelWidth, + snapPanelHeight + ); + } + // ── Visibility ──────────────────────────────────────────────────────── // The effect starts invisible; toggle via shortcut. @@ -28,14 +261,116 @@ KWinComponents.SceneEffect { name: "SHIFT Snap Assist" text: "SHIFT Snap Assist: Show snap layout picker" sequence: "Meta+Shift+S" - onActivated: effect.visible = !effect.visible + onActivated: { + if (!effect.snapLayoutsEligible) { + effect.hideSnapLayouts(); + return; + } + if (effect.visible) { + effect.hideSnapLayouts(); + } else { + effect.showSnapLayouts(); + } + } + } + + KWinComponents.ShortcutHandler { + name: "SHIFT Snap Assist Escape" + text: "SHIFT Snap Assist: Hide snap layout picker" + sequence: "Esc" + onActivated: effect.hideSnapLayouts() + } + + Timer { + id: hoverTimer + interval: effect.hoverTimerInterval + repeat: true + running: effect.snapLayoutsEligible + + onTriggered: { + if (effect.visible) { + const screen = KWinComponents.Workspace.activeWindow ? KWinComponents.Workspace.activeWindow.output : null; + if (!effect.cursorInActiveWindowMaximizeStrip() && !effect.cursorInPanel(screen)) { + effect.hideSnapLayouts(); + } else { + effect.resetHoverState(); + } + return; + } + + const win = KWinComponents.Workspace.activeWindow; + if (!win || !win.normalWindow || win.fullScreen || !win.maximizable) { + effect.resetHoverState(); + return; + } + + if (!effect.cursorInActiveWindowMaximizeStrip()) { + effect.resetHoverState(); + return; + } + + if (effect.hoverOnCooldown()) { + effect.resetHoverState(); + return; + } + + if (effect.hoverWindowId !== win.internalId) { + effect.resetHoverCandidate(win); + return; + } + + if (effect.hoverMovedTooFar()) { + effect.resetHoverCandidate(win); + return; + } + + const stateKey = effect.windowStateKey(win); + if (stateKey !== effect.hoverWindowStateKey) { + effect.hoverWindowStateKey = stateKey; + effect.hoverTicks = 0; + effect.hoverSuppressedUntilLeave = true; + effect.setHoverCooldown(); + return; + } + + if (effect.hoverSuppressedUntilLeave) { + return; + } + + effect.hoverTicks++; + if (effect.hoverTicks >= effect.hoverDwellTicks) { + effect.showSnapLayouts(); + effect.resetHoverState(); + } + } } - // Auto-hide when a window starts being moved (decoration hover path) Connections { target: KWinComponents.Workspace function onActiveWindowChanged() { - // Keep visible so decoration can re-trigger + effect.resetHoverState(); + } + } + + Connections { + target: ShellSettings.Settings + + function onConvergenceModeEnabledChanged() { + if (!effect.snapLayoutsEligible) { + effect.hideSnapLayouts(); + } + } + + function onGamingModeEnabledChanged() { + if (!effect.snapLayoutsEligible) { + effect.hideSnapLayouts(); + } + } + + function onDynamicTilingEnabledChanged() { + if (!effect.snapLayoutsEligible) { + effect.hideSnapLayouts(); + } } } @@ -46,7 +381,7 @@ KWinComponents.SceneEffect { // Each preset is an array of zone descriptors: // { x, y, w, h } in relative [0..1] coordinates (of work area). // The first zone is where the ACTIVE window will be placed. - // Remaining zones are shown as snap-assist targets. + // Remaining zones are currently visual-only. readonly property var presets: [ { @@ -97,6 +432,16 @@ KWinComponents.SceneEffect { } ] + readonly property int snapButtonWidth: 58 + readonly property int snapButtonHeight: 38 + readonly property int snapButtonSpacing: 8 + readonly property int snapPanelHorizontalPadding: 28 + readonly property int snapPanelVerticalPadding: 34 + readonly property int snapPanelWidth: presets.length * snapButtonWidth + + Math.max(0, presets.length - 1) * snapButtonSpacing + + snapPanelHorizontalPadding + readonly property int snapPanelHeight: snapButtonHeight + snapPanelVerticalPadding + 23 + // Apply a zone (in relative coords) to a window given a work area rect. function applyZone(win, zone, area) { const g = effect.outerGap; @@ -113,18 +458,103 @@ KWinComponents.SceneEffect { delegate: Rectangle { id: screenDelegate - // Transparent background — clicks outside the panel pass through + readonly property var targetScreen: KWinComponents.SceneView.screen + readonly property rect popupRect: effect.panelRect(targetScreen) + color: "transparent" - // The panel sits at the top-right of the screen - SnapPanel { - anchors { - top: parent.top - topMargin: 48 // below typical titlebar height - right: parent.right - rightMargin: 16 + KWinComponents.DesktopBackground { + anchors.fill: parent + z: -100 + activity: KWinComponents.Workspace.currentActivity + desktop: KWinComponents.Workspace.currentDesktop + outputName: screenDelegate.targetScreen.name + } + + Instantiator { + model: KWinComponents.WindowFilterModel { + desktop: KWinComponents.Workspace.currentDesktop + screenName: screenDelegate.targetScreen.name + windowModel: stackModel + minimizedWindows: false + windowType: ~KWinComponents.WindowFilterModel.Desktop + & ~KWinComponents.WindowFilterModel.Notification + & ~KWinComponents.WindowFilterModel.CriticalNotification } + KWinComponents.WindowThumbnail { + wId: model.window.internalId + x: model.window.x - screenDelegate.targetScreen.geometry.x + y: model.window.y - screenDelegate.targetScreen.geometry.y + z: model.window.stackingOrder + visible: !model.window.hidden + } + + onObjectAdded: (index, object) => { + object.parent = screenDelegate + } + } + + KWinComponents.WindowModel { + id: stackModel + } + + Repeater { + model: effect.previewVisible && effect.previewScreenName === screenDelegate.targetScreen.name ? effect.previewZones : [] + + delegate: Rectangle { + required property var modelData + required property int index + + readonly property bool activeZone: index === effect.previewActiveIndex + readonly property int previewGap: effect.outerGap + + x: effect.previewArea.x + Math.round(modelData.x * effect.previewArea.width) + previewGap - screenDelegate.targetScreen.geometry.x + y: effect.previewArea.y + Math.round(modelData.y * effect.previewArea.height) + previewGap - screenDelegate.targetScreen.geometry.y + width: Math.max(1, Math.round(modelData.w * effect.previewArea.width) - 2 * previewGap) + height: Math.max(1, Math.round(modelData.h * effect.previewArea.height) - 2 * previewGap) + z: activeZone ? 90001 : 90000 + radius: 6 + color: activeZone ? Qt.rgba(0.38, 0.60, 0.98, 0.24) + : Qt.rgba(0.88, 0.92, 1.0, 0.08) + border.width: activeZone ? 2 : 1 + border.color: activeZone ? Qt.rgba(0.70, 0.82, 1.0, 0.78) + : Qt.rgba(0.86, 0.90, 1.0, 0.26) + } + } + + Keys.onEscapePressed: effect.hideSnapLayouts() + + Item { + id: maximizeForwarder + + readonly property var activeWindow: KWinComponents.Workspace.activeWindow + readonly property rect buttonRect: activeWindow ? effect.maximizeButtonRect(activeWindow) : Qt.rect(0, 0, 0, 0) + + visible: activeWindow + && activeWindow.output + && activeWindow.output.name === screenDelegate.targetScreen.name + && buttonRect.width > 0 + && buttonRect.height > 0 + z: 100001 + x: buttonRect.x - effect.maximizeButtonPadding - screenDelegate.targetScreen.geometry.x + y: buttonRect.y - effect.maximizeButtonPadding - screenDelegate.targetScreen.geometry.y + width: buttonRect.width + 2 * effect.maximizeButtonPadding + height: buttonRect.height + 2 * effect.maximizeButtonPadding + + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: effect.toggleActiveWindowMaximized() + } + } + + SnapPanel { + id: snapPanel + visible: effect.panelAnchorScreenName === "" || effect.panelAnchorScreenName === screenDelegate.targetScreen.name + z: 100000 + x: screenDelegate.popupRect.x - screenDelegate.targetScreen.geometry.x + y: screenDelegate.popupRect.y - screenDelegate.targetScreen.geometry.y + screen: KWinComponents.SceneView.screen } } @@ -136,48 +566,47 @@ KWinComponents.SceneEffect { required property var screen - width: presetsRow.implicitWidth + 32 - height: presetsRow.implicitHeight + 40 + width: effect.snapPanelWidth + height: effect.snapPanelHeight - color: Qt.rgba(0.10, 0.12, 0.18, 0.92) - radius: 12 + color: Qt.rgba(0.08, 0.10, 0.15, 0.82) + radius: 8 border.width: 1 - border.color: Qt.rgba(1, 1, 1, 0.10) + border.color: Qt.rgba(1, 1, 1, 0.16) - // Drop shadow via a blurred copy underneath Rectangle { - anchors { fill: parent; margins: -6 } - color: Qt.rgba(0, 0, 0, 0.35) - radius: parent.radius + 6 + anchors.fill: parent + anchors.topMargin: 3 + color: Qt.rgba(0, 0, 0, 0.22) + radius: parent.radius z: -1 - layer.enabled: true - layer.effect: Item {} // placeholder — real blur requires Qt.labs.platform } Column { - anchors { fill: parent; margins: 16 } - spacing: 12 + anchors { fill: parent; margins: 14 } + spacing: 10 Text { text: "Snap layouts" color: "#b0b8d4" - font.pixelSize: 11 - font.letterSpacing: 0.8 + font.pixelSize: 10 font.capitalization: Font.AllUppercase } Row { id: presetsRow - spacing: 10 + spacing: effect.snapButtonSpacing Repeater { model: effect.presets - PresetButton { + delegate: PresetButton { + required property var modelData + preset: modelData screen: panel.screen onClicked: { - effect.visible = false; + effect.hideSnapLayouts(); } } } @@ -194,61 +623,73 @@ KWinComponents.SceneEffect { required property var screen signal clicked - width: 64 - height: 44 + width: effect.snapButtonWidth + height: effect.snapButtonHeight + readonly property int previewMargin: 6 - color: hovered ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(1, 1, 1, 0.06) + color: hovered ? Qt.rgba(1, 1, 1, 0.18) : Qt.rgba(1, 1, 1, 0.08) radius: 6 border.width: 1 - border.color: hovered ? Qt.rgba(1, 1, 1, 0.30) : Qt.rgba(1, 1, 1, 0.12) + border.color: hovered ? Qt.rgba(0.66, 0.78, 1.0, 0.62) : Qt.rgba(1, 1, 1, 0.14) property bool hovered: false Behavior on color { ColorAnimation { duration: 80 } } - // Mini zone diagram - Repeater { - model: btn.preset.zones + Rectangle { + id: previewFrame + anchors.fill: parent + anchors.margins: btn.previewMargin + color: Qt.rgba(0.06, 0.08, 0.12, 0.72) + radius: 4 + border.width: 1 + border.color: Qt.rgba(1, 1, 1, 0.08) + clip: true - Rectangle { - x: Math.round(modelData.x * (btn.width - 2)) + 1 - y: Math.round(modelData.y * (btn.height - 2)) + 1 - width: Math.round(modelData.w * (btn.width - 2)) - 1 - height: Math.round(modelData.h * (btn.height - 2)) - 1 - color: index === 0 ? Qt.rgba(0.44, 0.62, 1.0, 0.85) - : Qt.rgba(0.44, 0.62, 1.0, 0.35) - border.width: 0 - radius: 2 + Repeater { + model: btn.preset.zones + + delegate: Rectangle { + required property var modelData + required property int index + + x: Math.round(modelData.x * previewFrame.width) + y: Math.round(modelData.y * previewFrame.height) + width: Math.max(4, Math.round(modelData.w * previewFrame.width) - 1) + height: Math.max(4, Math.round(modelData.h * previewFrame.height) - 1) + color: zoneHover.hovered ? Qt.rgba(0.78, 0.86, 1.0, 0.96) + : (index === 0 ? Qt.rgba(0.46, 0.64, 0.96, 0.96) + : Qt.rgba(0.58, 0.68, 0.86, 0.48)) + border.width: 0 + radius: 2 + + HoverHandler { + id: zoneHover + onHoveredChanged: { + btn.hovered = hovered; + if (hovered) { + effect.showSnapPreview(btn.preset, index); + } + } + } + + TapHandler { + onTapped: btn.applyZone(modelData) + } + } } } HoverHandler { onHoveredChanged: { - btn.hovered = hovered; - if (hovered) { - // Show outline on screen for the first zone - const win = KWinComponents.Workspace.activeWindow; - if (!win || !win.output) return; - const desktop = win.desktops.length > 0 ? win.desktops[0] : null; - if (!desktop) return; - const area = KWinComponents.Workspace.clientArea( - KWinComponents.Workspace.MaximizeArea, win.output, desktop); - const zone = btn.preset.zones[0]; - const g = effect.outerGap; - KWinComponents.Workspace.showOutline(Qt.rect( - area.x + Math.round(zone.x * area.width) + g, - area.y + Math.round(zone.y * area.height) + g, - Math.round(zone.w * area.width) - 2 * g, - Math.round(zone.h * area.height) - 2 * g - )); - } else { - KWinComponents.Workspace.hideOutline(); + if (!hovered) { + btn.hovered = false; + effect.hideSnapPreview(); } } } - TapHandler { - onTapped: { + function applyZone(zone) { const win = KWinComponents.Workspace.activeWindow; if (!win || !win.output) { btn.clicked(); @@ -261,10 +702,9 @@ KWinComponents.SceneEffect { } const area = KWinComponents.Workspace.clientArea( KWinComponents.Workspace.MaximizeArea, win.output, desktop); - KWinComponents.Workspace.hideOutline(); - effect.applyZone(win, btn.preset.zones[0], area); + effect.hideSnapPreview(); + effect.applyZone(win, zone, area); btn.clicked(); - } } } } diff --git a/kwin/effects/shift-snap-assist/metadata.json b/kwin/effects/shift-snap-assist/metadata.json index 9aaad00e..101fcbcf 100644 --- a/kwin/effects/shift-snap-assist/metadata.json +++ b/kwin/effects/shift-snap-assist/metadata.json @@ -8,7 +8,7 @@ } ], "Category": "Appearance", - "Description": "Windows 11-style snap layout picker for SHIFT convergence mode. Activated by hovering the maximize button or pressing Meta+Shift+S.", + "Description": "Windows 11-style snap layout picker for SHIFT convergence mode with native window placement. Activated by hovering the maximize button or pressing Meta+Shift+S when dynamic tiling is off.", "EnabledByDefault": false, "Id": "shift-snap-assist", "License": "EUPL-1.2", diff --git a/kwin/scripts/shift-tiling/contents/ui/main.qml b/kwin/scripts/shift-tiling/contents/ui/main.qml index e760673b..3e136cac 100644 --- a/kwin/scripts/shift-tiling/contents/ui/main.qml +++ b/kwin/scripts/shift-tiling/contents/ui/main.qml @@ -595,67 +595,6 @@ Item { } } - // ── Snap-assist hover trigger ───────────────────────────────────────── - // - // The decoration QML sandbox has no DBus / kglobalaccel access, so we - // detect the maximize-button hover here in the script. We poll the - // cursor every 150 ms; when it stays in the top-right ~50×barHeight - // strip of the active window for 500 ms (without dragging), we invoke - // the SHIFT Snap Assist effect via kglobalaccel. - // - // Constants must match the decoration: - // barHeight 30, btnSize 16, btnSpacing 8, btnSideMargin 12. - // Right-cluster width ≈ 12 (margin) + 3·(16+8) = ~84 px. We use 90 px - // to be forgiving. - - readonly property int hoverBarHeight: 30 - readonly property int hoverButtonStrip: 90 - property var hoverWindowId: null - property int hoverTicks: 0 - - Timer { - id: snapHoverTimer - interval: 150 - repeat: true - running: root.tilingEnabled && root.isConvergence() - - onTriggered: { - // Don't fire while dragging or while no window is focused. - if (root.draggingWindow) { root.hoverTicks = 0; root.hoverWindowId = null; return; } - const win = KWinComponents.Workspace.activeWindow; - if (!win || !win.normalWindow || win.fullScreen) { - root.hoverTicks = 0; root.hoverWindowId = null; return; - } - const cursor = KWinComponents.Workspace.cursorPos; - const fg = win.frameGeometry; - // Right-side titlebar strip in absolute coords. - const stripX = fg.x + fg.width - root.hoverButtonStrip; - const stripY = fg.y; - if (cursor.x >= stripX && cursor.x <= fg.x + fg.width && - cursor.y >= stripY && cursor.y <= stripY + root.hoverBarHeight) { - if (root.hoverWindowId === win.internalId) { - root.hoverTicks++; - // 500 ms ≈ 4 ticks at 150 ms (3 + 1 to be safe). - if (root.hoverTicks === 4) { - callDBus( - "org.kde.kglobalaccel", - "/component/kwin", - "org.kde.kglobalaccel.Component", - "invokeShortcut", - "SHIFT Snap Assist" - ); - } - } else { - root.hoverWindowId = win.internalId; - root.hoverTicks = 1; - } - } else { - root.hoverTicks = 0; - root.hoverWindowId = null; - } - } - } - // ── Right-click menu ────────────────────────────────────────────────── // Note: registerUserActionsMenu is a global function in KWin JS scripts.