shift-shell/kwin/effects/shift-snap-assist/contents/ui/main.qml

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

271 lines
9.3 KiB
QML
Raw Normal View History

// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
//
// SHIFT Snap Assist — KWin declarative SceneEffect
//
// 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
//
// 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.
import QtQuick
import QtQuick.Layouts
import org.kde.kwin as KWinComponents
KWinComponents.SceneEffect {
id: effect
// ── Visibility ────────────────────────────────────────────────────────
// The effect starts invisible; toggle via shortcut.
// SceneEffect.visible controls whether delegates are painted.
KWinComponents.ShortcutHandler {
name: "SHIFT Snap Assist"
text: "SHIFT Snap Assist: Show snap layout picker"
sequence: "Meta+Shift+S"
onActivated: effect.visible = !effect.visible
}
// Auto-hide when a window starts being moved (decoration hover path)
Connections {
target: KWinComponents.Workspace
function onActiveWindowChanged() {
// Keep visible so decoration can re-trigger
}
}
// ── Gap constant (must match shift-tiling) ────────────────────────────
readonly property int outerGap: 8
// ── Layout presets ────────────────────────────────────────────────────
// 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.
readonly property var presets: [
{
name: "Half left",
zones: [
{ x: 0, y: 0, w: 0.5, h: 1 },
{ x: 0.5, y: 0, w: 0.5, h: 1 }
]
},
{
name: "Thirds",
zones: [
{ x: 0, y: 0, w: 0.333, h: 1 },
{ x: 0.333, y: 0, w: 0.334, h: 1 },
{ x: 0.667, y: 0, w: 0.333, h: 1 }
]
},
{
name: "Main + side",
zones: [
{ x: 0, y: 0, w: 0.667, h: 1 },
{ x: 0.667, y: 0, w: 0.333, h: 1 }
]
},
{
name: "Side + main",
zones: [
{ x: 0.333, y: 0, w: 0.667, h: 1 },
{ x: 0, y: 0, w: 0.333, h: 1 }
]
},
{
name: "Quad",
zones: [
{ x: 0, y: 0, w: 0.5, h: 0.5 },
{ x: 0.5, y: 0, w: 0.5, h: 0.5 },
{ x: 0, y: 0.5, w: 0.5, h: 0.5 },
{ x: 0.5, y: 0.5, w: 0.5, h: 0.5 }
]
},
{
name: "Main + two",
zones: [
{ x: 0, y: 0, w: 0.5, h: 1 },
{ x: 0.5, y: 0, w: 0.5, h: 0.5 },
{ x: 0.5, y: 0.5, w: 0.5, h: 0.5 }
]
}
]
// Apply a zone (in relative coords) to a window given a work area rect.
function applyZone(win, zone, area) {
const g = effect.outerGap;
win.frameGeometry = 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
);
}
// ── Per-screen delegate ───────────────────────────────────────────────
delegate: Rectangle {
id: screenDelegate
// Transparent background — clicks outside the panel pass through
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
}
screen: KWinComponents.SceneView.screen
}
}
// ── Snap panel component ──────────────────────────────────────────────
component SnapPanel: Rectangle {
id: panel
required property var screen
width: presetsRow.implicitWidth + 32
height: presetsRow.implicitHeight + 40
color: Qt.rgba(0.10, 0.12, 0.18, 0.92)
radius: 12
border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.10)
// 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
z: -1
layer.enabled: true
layer.effect: Item {} // placeholder — real blur requires Qt.labs.platform
}
Column {
anchors { fill: parent; margins: 16 }
spacing: 12
Text {
text: "Snap layouts"
color: "#b0b8d4"
font.pixelSize: 11
font.letterSpacing: 0.8
font.capitalization: Font.AllUppercase
}
Row {
id: presetsRow
spacing: 10
Repeater {
model: effect.presets
PresetButton {
preset: modelData
screen: panel.screen
onClicked: {
effect.visible = false;
}
}
}
}
}
}
// ── Preset button ─────────────────────────────────────────────────────
component PresetButton: Rectangle {
id: btn
required property var preset
required property var screen
signal clicked
width: 64
height: 44
color: hovered ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(1, 1, 1, 0.06)
radius: 6
border.width: 1
border.color: hovered ? Qt.rgba(1, 1, 1, 0.30) : Qt.rgba(1, 1, 1, 0.12)
property bool hovered: false
Behavior on color { ColorAnimation { duration: 80 } }
// Mini zone diagram
Repeater {
model: btn.preset.zones
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
}
}
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();
}
}
}
TapHandler {
onTapped: {
const win = KWinComponents.Workspace.activeWindow;
if (!win || !win.output) {
btn.clicked();
return;
}
const desktop = win.desktops.length > 0 ? win.desktops[0] : null;
if (!desktop) {
btn.clicked();
return;
}
const area = KWinComponents.Workspace.clientArea(
KWinComponents.Workspace.MaximizeArea, win.output, desktop);
KWinComponents.Workspace.hideOutline();
effect.applyZone(win, btn.preset.zones[0], area);
btn.clicked();
}
}
}
}