mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 16:57:43 +00:00
Expose snapLayoutsEnabled through MobileShellSettings and store it\nin plasmamobilerc.\n\nUse the setting in the Shift snap assist effect eligibility logic\nand add a Convergence KCM switch to control it.\n\nKeep the switch disabled when convergence is off, gaming mode is on,\nor dynamic tiling is enabled so window-placement ownership stays\nconsistent.
725 lines
26 KiB
QML
725 lines
26 KiB
QML
// 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. 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).
|
|
|
|
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
|
|
&& ShellSettings.Settings.snapLayoutsEnabled
|
|
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.
|
|
// 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: {
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: KWinComponents.Workspace
|
|
function onActiveWindowChanged() {
|
|
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();
|
|
}
|
|
}
|
|
|
|
function onSnapLayoutsEnabledChanged() {
|
|
if (!effect.snapLayoutsEligible) {
|
|
effect.hideSnapLayouts();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 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 currently visual-only.
|
|
|
|
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 }
|
|
]
|
|
}
|
|
]
|
|
|
|
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
|
|
|
|
function accentColor(alpha) {
|
|
return Qt.rgba(46 / 255, 184 / 255, 168 / 255, alpha);
|
|
}
|
|
|
|
function accentPaleColor(alpha) {
|
|
return Qt.rgba(163 / 255, 218 / 255, 212 / 255, alpha);
|
|
}
|
|
|
|
// 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
|
|
|
|
readonly property var targetScreen: KWinComponents.SceneView.screen
|
|
readonly property rect popupRect: effect.panelRect(targetScreen)
|
|
|
|
color: "transparent"
|
|
|
|
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 ? effect.accentColor(0.24)
|
|
: effect.accentPaleColor(0.08)
|
|
border.width: activeZone ? 2 : 1
|
|
border.color: activeZone ? effect.accentPaleColor(0.78)
|
|
: effect.accentPaleColor(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
|
|
}
|
|
}
|
|
|
|
// ── Snap panel component ──────────────────────────────────────────────
|
|
|
|
component SnapPanel: Rectangle {
|
|
id: panel
|
|
|
|
required property var screen
|
|
|
|
width: effect.snapPanelWidth
|
|
height: effect.snapPanelHeight
|
|
|
|
color: Qt.rgba(0.08, 0.10, 0.15, 0.82)
|
|
radius: 8
|
|
border.width: 1
|
|
border.color: Qt.rgba(1, 1, 1, 0.16)
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.topMargin: 3
|
|
color: Qt.rgba(0, 0, 0, 0.22)
|
|
radius: parent.radius
|
|
z: -1
|
|
}
|
|
|
|
Column {
|
|
anchors { fill: parent; margins: 14 }
|
|
spacing: 10
|
|
|
|
Text {
|
|
text: "Snap layouts"
|
|
color: effect.accentPaleColor(0.9)
|
|
font.pixelSize: 10
|
|
font.capitalization: Font.AllUppercase
|
|
}
|
|
|
|
Row {
|
|
id: presetsRow
|
|
spacing: effect.snapButtonSpacing
|
|
|
|
Repeater {
|
|
model: effect.presets
|
|
|
|
delegate: PresetButton {
|
|
required property var modelData
|
|
|
|
preset: modelData
|
|
screen: panel.screen
|
|
onClicked: {
|
|
effect.hideSnapLayouts();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Preset button ─────────────────────────────────────────────────────
|
|
|
|
component PresetButton: Rectangle {
|
|
id: btn
|
|
|
|
required property var preset
|
|
required property var screen
|
|
signal clicked
|
|
|
|
width: effect.snapButtonWidth
|
|
height: effect.snapButtonHeight
|
|
readonly property int previewMargin: 6
|
|
|
|
color: hovered ? Qt.rgba(1, 1, 1, 0.18) : Qt.rgba(1, 1, 1, 0.08)
|
|
radius: 6
|
|
border.width: 1
|
|
border.color: hovered ? effect.accentPaleColor(0.62) : Qt.rgba(1, 1, 1, 0.14)
|
|
|
|
property bool hovered: false
|
|
|
|
Behavior on color { ColorAnimation { duration: 80 } }
|
|
|
|
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
|
|
|
|
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 ? effect.accentPaleColor(0.96)
|
|
: (index === 0 ? effect.accentColor(0.96)
|
|
: effect.accentPaleColor(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: {
|
|
if (!hovered) {
|
|
btn.hovered = false;
|
|
effect.hideSnapPreview();
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyZone(zone) {
|
|
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);
|
|
effect.hideSnapPreview();
|
|
effect.applyZone(win, zone, area);
|
|
btn.clicked();
|
|
}
|
|
}
|
|
}
|