From 7eb7cecf1e950504b2e4bf8f2acf97069c0e0300 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Sat, 7 Mar 2026 18:13:07 +0100 Subject: [PATCH 01/47] smart --- Helpers/Wallpapers.qml | 6 ++-- cli/src/zshell/subcommands/scheme.py | 53 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index 498bb5f..afbe9e8 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -19,20 +19,20 @@ Searcher { function preview(path: string): void { previewPath = path; if (Config.general.color.schemeGeneration) - Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${previewPath} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]); + Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${previewPath}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); showPreview = true; } function setWallpaper(path: string): void { actualCurrent = path; WallpaperPath.currentWallpaperPath = path; - Quickshell.execDetached(["sh", "-c", `zshell-cli wallpaper lockscreen --input-image=${root.actualCurrent} --output-path=${Paths.state}/lockscreen_bg.png --blur-amount=${Config.lock.blurAmount}`]); + Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); } function stopPreview(): void { showPreview = false; if (Config.general.color.schemeGeneration) - Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${root.actualCurrent} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]); + Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); } extraOpts: useFuzzy ? ({}) : ({ diff --git a/cli/src/zshell/subcommands/scheme.py b/cli/src/zshell/subcommands/scheme.py index 2b6e3fd..63ab1e3 100644 --- a/cli/src/zshell/subcommands/scheme.py +++ b/cli/src/zshell/subcommands/scheme.py @@ -2,6 +2,8 @@ import typer import json import shutil import os +import re +import subprocess from jinja2 import Environment, FileSystemLoader, StrictUndefined, Undefined from typing import Any, Optional, Tuple @@ -240,6 +242,53 @@ def generate( return is_dark + def apply_gtk_mode(mode: str) -> None: + mode = mode.lower() + preference = "prefer-dark" if mode == "dark" else "prefer-light" + + try: + subprocess.run( + [ + "gsettings", + "set", + "org.gnome.desktop.interface", + "color-scheme", + preference, + ], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except FileNotFoundError: + pass + + def apply_qt_mode(mode: str, home: str) -> None: + mode = mode.lower() + qt_conf = Path(home) / ".config/qt6ct/qt6ct.conf" + + if not qt_conf.exists(): + return + + try: + text = qt_conf.read_text(encoding="utf-8") + except OSError: + return + + target = "Dark.colors" if mode == "dark" else "Light.colors" + + new_text, count = re.subn( + r"^(color_scheme_path=.*?)(?:Light|Dark)\.colors\s*$", + rf"\1{target}", + text, + flags=re.MULTILINE, + ) + + if count > 0 and new_text != text: + try: + qt_conf.write_text(new_text, encoding="utf-8") + except OSError: + pass + def build_template_context( *, colors: dict[str, str], @@ -440,6 +489,10 @@ def generate( colors = generate_color_scheme(seed, effective_mode, scheme_class) + if smart and not preset: + apply_gtk_mode(mode) + apply_qt_mode(mode, HOME) + output_dict = { "name": name, "flavor": flavor, -- 2.47.3 From 1b0eb9fdb2d6eb432f63f7509b5aed7aaa6eace4 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Sat, 7 Mar 2026 18:14:45 +0100 Subject: [PATCH 02/47] smart --- cli/src/zshell/subcommands/scheme.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/zshell/subcommands/scheme.py b/cli/src/zshell/subcommands/scheme.py index 63ab1e3..32520f3 100644 --- a/cli/src/zshell/subcommands/scheme.py +++ b/cli/src/zshell/subcommands/scheme.py @@ -490,8 +490,8 @@ def generate( colors = generate_color_scheme(seed, effective_mode, scheme_class) if smart and not preset: - apply_gtk_mode(mode) - apply_qt_mode(mode, HOME) + apply_gtk_mode(effective_mode) + apply_qt_mode(effective_mode, HOME) output_dict = { "name": name, -- 2.47.3 From 902a1adbfe381fc4c3ced074d68f48320801df9c Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Sun, 8 Mar 2026 10:06:04 +0100 Subject: [PATCH 03/47] rounding --- Helpers/ModeScheduler.qml | 2 +- Modules/Dashboard/Background.qml | 2 +- Modules/Dashboard/Content.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Helpers/ModeScheduler.qml b/Helpers/ModeScheduler.qml index e4851f5..9ca6d40 100644 --- a/Helpers/ModeScheduler.qml +++ b/Helpers/ModeScheduler.qml @@ -33,7 +33,7 @@ Singleton { } function applyLightMode() { - if (Config.general.color.neovimColors) { + if (Config.general.color.schemeGeneration) { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${WallpaperPath.currentWallpaperPath}`, "--thumbnail-path", `${Paths.cache}/imagecache/thumbnail.jpg`, "--output", `${Paths.state}/scheme.json`, "--scheme", `${Config.colors.schemeType}`, "--mode", "light"]); } else { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${DynamicColors.scheme}:${DynamicColors.flavour}`, "--output", `${Paths.state}/scheme.json`, "--mode", "light"]); diff --git a/Modules/Dashboard/Background.qml b/Modules/Dashboard/Background.qml index b97793b..cf42103 100644 --- a/Modules/Dashboard/Background.qml +++ b/Modules/Dashboard/Background.qml @@ -7,7 +7,7 @@ ShapePath { id: root readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real rounding: 8 + readonly property real rounding: Appearance.rounding.normal readonly property real roundingY: flatten ? wrapper.height / 2 : rounding required property Wrapper wrapper diff --git a/Modules/Dashboard/Content.qml b/Modules/Dashboard/Content.qml index ad1fd3d..867004e 100644 --- a/Modules/Dashboard/Content.qml +++ b/Modules/Dashboard/Content.qml @@ -40,7 +40,7 @@ Item { anchors.right: parent.right anchors.top: parent.top color: "transparent" - radius: 6 + radius: Appearance.rounding.normal - anchors.margins Item { id: view -- 2.47.3 From a9fc2cbf69b5110f6eead40d222b91cbc36c69fc Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 9 Mar 2026 00:35:18 +0100 Subject: [PATCH 04/47] vulkan, screenshotting broken --- Modules/Bar/BarLoader.qml | 1 - Modules/WindowTitle.qml | 1 - shell.qml | 2 ++ 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Bar/BarLoader.qml b/Modules/Bar/BarLoader.qml index 2c3a70a..67e27e5 100644 --- a/Modules/Bar/BarLoader.qml +++ b/Modules/Bar/BarLoader.qml @@ -166,7 +166,6 @@ RowLayout { delegate: WrappedLoader { sourceComponent: WindowTitle { bar: root - monitor: Brightness.getMonitorForScreen(root.screen) } } } diff --git a/Modules/WindowTitle.qml b/Modules/WindowTitle.qml index 68de9db..c5c1a3b 100644 --- a/Modules/WindowTitle.qml +++ b/Modules/WindowTitle.qml @@ -19,7 +19,6 @@ Item { }, 0); return bar.width - otherWidth - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2; } - required property Brightness.Monitor monitor clip: true implicitHeight: current.implicitHeight diff --git a/shell.qml b/shell.qml index 3d19be0..37ba051 100644 --- a/shell.qml +++ b/shell.qml @@ -1,5 +1,7 @@ //@ pragma UseQApplication //@ pragma Env QSG_RENDER_LOOP=threaded +//@ pragma Env QSG_RHI_BACKEND=vulkan +//@ pragma Env QSG_USE_SIMPLE_ANIMATION_DRIVER=1 //@ pragma Env QS_NO_RELOAD_POPUP=1 import Quickshell import qs.Modules -- 2.47.3 From 720bc2808e6c43e95ca90fbed7d4a5f6e95aaffa Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 9 Mar 2026 00:36:18 +0100 Subject: [PATCH 05/47] back to opengl --- shell.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/shell.qml b/shell.qml index 37ba051..85b9c7c 100644 --- a/shell.qml +++ b/shell.qml @@ -1,6 +1,5 @@ //@ pragma UseQApplication //@ pragma Env QSG_RENDER_LOOP=threaded -//@ pragma Env QSG_RHI_BACKEND=vulkan //@ pragma Env QSG_USE_SIMPLE_ANIMATION_DRIVER=1 //@ pragma Env QS_NO_RELOAD_POPUP=1 import Quickshell -- 2.47.3 From 1ee345f946a162cd5baf911cf0f7efe27c423d06 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 9 Mar 2026 12:47:09 +0100 Subject: [PATCH 06/47] drawing --- Components/ColorArcPicker.qml | 170 ++++++++++++++++++++++++++++++ Drawers/Backgrounds.qml | 8 +- Drawers/Bar.qml | 46 ++++++-- Drawers/Drawing.qml | 186 +++++++++++++++++++++++++++++++++ Drawers/DrawingInput.qml | 53 ++++++++++ Drawers/Interactions.qml | 117 +++------------------ Drawers/Panels.qml | 13 +++ Modules/Drawing/Background.qml | 66 ++++++++++++ Modules/Drawing/Content.qml | 76 ++++++++++++++ Modules/Drawing/Wrapper.qml | 133 +++++++++++++++++++++++ Modules/Shortcuts.qml | 9 ++ 11 files changed, 762 insertions(+), 115 deletions(-) create mode 100644 Components/ColorArcPicker.qml create mode 100644 Drawers/Drawing.qml create mode 100644 Drawers/DrawingInput.qml create mode 100644 Modules/Drawing/Background.qml create mode 100644 Modules/Drawing/Content.qml create mode 100644 Modules/Drawing/Wrapper.qml diff --git a/Components/ColorArcPicker.qml b/Components/ColorArcPicker.qml new file mode 100644 index 0000000..3e3a182 --- /dev/null +++ b/Components/ColorArcPicker.qml @@ -0,0 +1,170 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +Item { + id: root + + readonly property real arcStartAngle: 0.75 * Math.PI + readonly property real arcSweep: 1.5 * Math.PI + property real currentHue: 0 + required property var drawing + property real handleSize: 30 + property real lastChromaticHue: 0 + property real ringThickness: 12 + readonly property int segmentCount: 180 + + function hueToAngle(hue) { + return arcStartAngle + arcSweep * hue; + } + + function normalizeAngle(angle) { + const tau = Math.PI * 2; + let a = angle % tau; + if (a < 0) + a += tau; + return a; + } + + function syncFromPenColor() { + if (!drawing) + return; + + const c = drawing.penColor; + + // QML color exposes HSL channels directly. + // If the current color is chromatic, move the handle to that hue. + // If it is achromatic (black/white/gray), keep the last useful hue. + if (c.hslSaturation > 0) { + currentHue = c.hslHue; + lastChromaticHue = c.hslHue; + } else { + currentHue = lastChromaticHue; + } + + canvas.requestPaint(); + } + + function updateHueFromPoint(x, y) { + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const dx = x - cx; + const dy = y - cy; + + const distance = Math.sqrt(dx * dx + dy * dy); + const radius = (Math.min(canvas.width, canvas.height) - handleSize - 8) / 2; + + if (distance < radius - 24 || distance > radius + 24) + return; + + const angle = normalizeAngle(Math.atan2(dy, dx)); + const start = normalizeAngle(arcStartAngle); + + let relative = angle - start; + if (relative < 0) + relative += Math.PI * 2; + + if (relative > arcSweep) { + const gap = Math.PI * 2 - arcSweep; + relative = relative < arcSweep + gap / 2 ? arcSweep : 0; + } + + currentHue = relative / arcSweep; + lastChromaticHue = currentHue; + drawing.penColor = Qt.hsla(currentHue, 1.0, 0.5, 1.0); + } + + implicitHeight: 180 + implicitWidth: 220 + + Component.onCompleted: syncFromPenColor() + onCurrentHueChanged: canvas.requestPaint() + onDrawingChanged: syncFromPenColor() + + Connections { + function onPenColorChanged() { + root.syncFromPenColor(); + } + + target: root.drawing + } + + Canvas { + id: canvas + + anchors.fill: parent + renderStrategy: Canvas.Threaded + renderTarget: Canvas.Image + + Component.onCompleted: requestPaint() + onHeightChanged: requestPaint() + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + const cx = width / 2; + const cy = height / 2; + const radius = (Math.min(width, height) - root.handleSize - 8) / 2; + + ctx.beginPath(); + ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); + ctx.lineWidth = root.ringThickness + 4; + ctx.lineCap = "round"; + ctx.strokeStyle = Qt.rgba(1, 1, 1, 0.12); + ctx.stroke(); + + for (let i = 0; i < root.segmentCount; ++i) { + const t1 = i / root.segmentCount; + const t2 = (i + 1) / root.segmentCount; + const a1 = root.arcStartAngle + root.arcSweep * t1; + const a2 = root.arcStartAngle + root.arcSweep * t2; + + ctx.beginPath(); + ctx.arc(cx, cy, radius, a1, a2); + ctx.lineWidth = root.ringThickness; + ctx.lineCap = "round"; + ctx.strokeStyle = Qt.hsla(t1, 1.0, 0.5, 1.0); + ctx.stroke(); + } + + const handleAngle = root.hueToAngle(root.currentHue); + const hx = cx + radius * Math.cos(handleAngle); + const hy = cy + radius * Math.sin(handleAngle); + + ctx.beginPath(); + ctx.arc(hx, hy, root.handleSize / 2, 0, Math.PI * 2); + ctx.fillStyle = Qt.rgba(1, 1, 1, 0.95); + ctx.fill(); + + ctx.beginPath(); + ctx.arc(hx, hy, root.handleSize / 2, 0, Math.PI * 2); + ctx.lineWidth = 1.5; + ctx.strokeStyle = Qt.rgba(0, 0, 0, 0.18); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(hx, hy, root.handleSize / 2 - 6, 0, Math.PI * 2); + ctx.fillStyle = root.drawing.penColor; + ctx.fill(); + + ctx.beginPath(); + ctx.arc(hx, hy, root.handleSize / 2 - 6, 0, Math.PI * 2); + ctx.lineWidth = 1; + ctx.strokeStyle = Qt.rgba(0, 0, 0, 0.20); + ctx.stroke(); + } + onWidthChanged: requestPaint() + } + + MouseArea { + acceptedButtons: Qt.LeftButton + anchors.fill: parent + + onPositionChanged: mouse => { + if (mouse.buttons & Qt.LeftButton) + root.updateHueFromPoint(mouse.x, mouse.y); + } + onPressed: mouse => root.updateHueFromPoint(mouse.x, mouse.y) + } +} diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index bcd1488..2eab002 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -11,7 +11,7 @@ import qs.Modules.Dashboard as Dashboard import qs.Modules.Osd as Osd import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources - +import qs.Modules.Drawing as Drawing import qs.Modules.Settings as Settings Shape { @@ -31,6 +31,12 @@ Shape { } } + Drawing.Background { + startX: 0 + startY: wrapper.y - rounding + wrapper: root.panels.drawing + } + Resources.Background { startX: 0 - rounding startY: 0 diff --git a/Drawers/Bar.qml b/Drawers/Bar.qml index 02d3e3c..be1bd99 100644 --- a/Drawers/Bar.qml +++ b/Drawers/Bar.qml @@ -32,19 +32,9 @@ Variants { WlrLayershell.namespace: "ZShell-Bar" color: "transparent" contentItem.focus: true + mask: visibilities.isDrawing ? null : region screen: scope.modelData - mask: Region { - id: region - - height: bar.screen.height - backgroundRect.implicitHeight - intersection: Intersection.Xor - regions: popoutRegions.instances - width: bar.width - x: 0 - y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height - } - contentItem.Keys.onEscapePressed: { if (Config.barConfig.autoHide) visibilities.bar = false; @@ -55,6 +45,17 @@ Variants { visibilities.resources = false; } + Region { + id: region + + height: bar.screen.height - backgroundRect.implicitHeight + intersection: Intersection.Xor + regions: popoutRegions.instances + width: bar.width + x: 0 + y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height + } + PanelWindow { id: exclusionZone @@ -117,6 +118,7 @@ Variants { property bool bar property bool dashboard + property bool isDrawing property bool launcher property bool notif: NotifServer.popups.length > 0 property bool osd @@ -157,20 +159,42 @@ Variants { } } + Drawing { + id: drawing + + anchors.fill: parent + z: 2 + } + + DrawingInput { + id: input + + bar: backgroundRect + drawing: drawing + panels: panels + popout: panels.drawing + visibilities: visibilities + z: 2 + } + Interactions { id: mouseArea anchors.fill: parent bar: barLoader + drawing: drawing + input: input panels: panels popouts: panels.popouts screen: scope.modelData visibilities: visibilities + z: 1 Panels { id: panels bar: backgroundRect + drawingItem: drawing screen: scope.modelData visibilities: visibilities } diff --git a/Drawers/Drawing.qml b/Drawers/Drawing.qml new file mode 100644 index 0000000..6a30ff2 --- /dev/null +++ b/Drawers/Drawing.qml @@ -0,0 +1,186 @@ +import QtQuick + +Canvas { + id: root + + property rect dirtyRect: Qt.rect(0, 0, 0, 0) + property bool frameQueued: false + property bool fullRepaintPending: true + property point lastPoint: Qt.point(0, 0) + property real minPointDistance: 2.0 + property color penColor: "white" + property real penWidth: 4 + property var pendingSegments: [] + property bool strokeActive: false + property var strokes: [] + + function appendPoint(x, y) { + if (!strokeActive || strokes.length === 0) + return; + const dx = x - lastPoint.x; + const dy = y - lastPoint.y; + + if ((dx * dx + dy * dy) < (minPointDistance * minPointDistance)) + return; + const x1 = lastPoint.x; + const y1 = lastPoint.y; + const x2 = x; + const y2 = y; + + strokes[strokes.length - 1].push(Qt.point(x2, y2)); + + pendingSegments.push({ + dot: false, + x1: x1, + y1: y1, + x2: x2, + y2: y2 + }); + + lastPoint = Qt.point(x2, y2); + queueDirty(segmentDirtyRect(x1, y1, x2, y2)); + } + + function beginStroke(x, y) { + const p = Qt.point(x, y); + strokes.push([p]); + lastPoint = p; + strokeActive = true; + + pendingSegments.push({ + dot: true, + x: x, + y: y + }); + + queueDirty(pointDirtyRect(x, y)); + } + + function clear() { + strokes = []; + pendingSegments = []; + dirtyRect = Qt.rect(0, 0, 0, 0); + fullRepaintPending = true; + markDirty(Qt.rect(0, 0, width, height)); + } + + function drawDot(ctx, x, y) { + ctx.beginPath(); + ctx.arc(x, y, penWidth / 2, 0, Math.PI * 2); + ctx.fill(); + } + + function drawSegment(ctx, x1, y1, x2, y2) { + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + } + + function endStroke() { + strokeActive = false; + } + + function pointDirtyRect(x, y) { + const pad = penWidth + 2; + return Qt.rect(x - pad, y - pad, pad * 2, pad * 2); + } + + function queueDirty(r) { + dirtyRect = unionRects(dirtyRect, r); + + if (frameQueued) + return; + frameQueued = true; + + requestAnimationFrame(function () { + frameQueued = false; + + if (dirtyRect.width > 0 && dirtyRect.height > 0) { + markDirty(dirtyRect); + dirtyRect = Qt.rect(0, 0, 0, 0); + } + }); + } + + function replayAll(ctx) { + ctx.clearRect(0, 0, width, height); + + for (const stroke of strokes) { + if (!stroke || stroke.length === 0) + continue; + if (stroke.length === 1) { + const p = stroke[0]; + drawDot(ctx, p.x, p.y); + continue; + } + + ctx.beginPath(); + ctx.moveTo(stroke[0].x, stroke[0].y); + for (let i = 1; i < stroke.length; ++i) + ctx.lineTo(stroke[i].x, stroke[i].y); + ctx.stroke(); + } + } + + function requestFullRepaint() { + fullRepaintPending = true; + markDirty(Qt.rect(0, 0, width, height)); + } + + function segmentDirtyRect(x1, y1, x2, y2) { + const pad = penWidth + 2; + const left = Math.min(x1, x2) - pad; + const top = Math.min(y1, y2) - pad; + const right = Math.max(x1, x2) + pad; + const bottom = Math.max(y1, y2) + pad; + return Qt.rect(left, top, right - left, bottom - top); + } + + function unionRects(a, b) { + if (a.width <= 0 || a.height <= 0) + return b; + if (b.width <= 0 || b.height <= 0) + return a; + + const left = Math.min(a.x, b.x); + const top = Math.min(a.y, b.y); + const right = Math.max(a.x + a.width, b.x + b.width); + const bottom = Math.max(a.y + a.height, b.y + b.height); + + return Qt.rect(left, top, right - left, bottom - top); + } + + anchors.fill: parent + contextType: "2d" + renderStrategy: Canvas.Threaded + renderTarget: Canvas.Image + + onHeightChanged: requestFullRepaint() + onPaint: region => { + const ctx = getContext("2d"); + + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.lineWidth = penWidth; + ctx.strokeStyle = penColor; + ctx.fillStyle = penColor; + + if (fullRepaintPending) { + fullRepaintPending = false; + replayAll(ctx); + pendingSegments = []; + return; + } + + for (const seg of pendingSegments) { + if (seg.dot) + drawDot(ctx, seg.x, seg.y); + else + drawSegment(ctx, seg.x1, seg.y1, seg.x2, seg.y2); + } + + pendingSegments = []; + } + onWidthChanged: requestFullRepaint() +} diff --git a/Drawers/DrawingInput.qml b/Drawers/DrawingInput.qml new file mode 100644 index 0000000..51a785f --- /dev/null +++ b/Drawers/DrawingInput.qml @@ -0,0 +1,53 @@ +import Quickshell +import QtQuick +import qs.Components + +CustomMouseArea { + id: root + + required property var bar + required property Drawing drawing + required property Panels panels + required property var popout + required property PersistentProperties visibilities + + function inLeftPanel(panel: Item, x: real, y: real): bool { + return x < panel.x + panel.width && withinPanelHeight(panel, x, y); + } + + function withinPanelHeight(panel: Item, x: real, y: real): bool { + const panelY = panel.y + bar.implicitHeight; + return y >= panelY && y <= panelY + panel.height; + } + + anchors.fill: root.visibilities.isDrawing ? parent : undefined + hoverEnabled: true + visible: root.visibilities.isDrawing + + onPositionChanged: event => { + const x = event.x; + const y = event.y; + + if (event.buttons & Qt.LeftButton) + root.drawing.appendPoint(x, y); + + if (root.inLeftPanel(root.popout, x, y)) { + root.z = -2; + root.panels.drawing.expanded = true; + } + } + onPressed: event => { + const x = event.x; + const y = event.y; + + if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) { + root.panels.drawing.expanded = false; + root.drawing.beginStroke(x, y); + return; + } + } + onReleased: { + if (root.visibilities.isDrawing) + root.drawing.endStroke(); + } +} diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 11f6561..79b0f72 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -10,6 +10,8 @@ CustomMouseArea { required property Item bar property bool dashboardShortcutActive property point dragStart + required property Drawing drawing + required property DrawingInput input property bool osdShortcutActive required property Panels panels required property BarPopouts.Wrapper popouts @@ -51,20 +53,10 @@ CustomMouseArea { anchors.fill: parent hoverEnabled: true - - // onPressed: event => { - // if ( root.popouts.hasCurrent && !inTopPanel( root.popouts, event.x, event.y )) { - // root.popouts.hasCurrent = false; - // } else if (root.visibilities.sidebar && !inRightPanel( panels.sidebar, event.x, event.y )) { - // root.visibilities.sidebar = false; - // } else if (root.visibilities.dashboard && !inTopPanel( panels.dashboard, event.x, event.y )) { - // root.visibilities.dashboard = false; - // } - // } + propagateComposedEvents: true onContainsMouseChanged: { if (!containsMouse) { - // Only hide if not activated by shortcut if (!osdShortcutActive) { visibilities.osd = false; root.panels.osd.hovered = false; @@ -87,120 +79,42 @@ CustomMouseArea { const dragX = x - dragStart.x; const dragY = y - dragStart.y; - // Show bar in non-exclusive mode on hover + if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) { + root.input.z = 2; + root.panels.drawing.expanded = false; + } + if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight + bar.anchors.topMargin) visibilities.bar = true; if (panels.sidebar.width === 0) { - // Show osd on hover const showOsd = inRightPanel(panels.osd, x, y); - // // Always update visibility based on hover if not in shortcut mode - if (!osdShortcutActive) { - visibilities.osd = showOsd; - root.panels.osd.hovered = showOsd; - } else if (showOsd) { - // If hovering over OSD area while in shortcut mode, transition to hover control + if (showOsd) { osdShortcutActive = false; root.panels.osd.hovered = true; } - - // const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; - // - // // Show/hide session on drag - // if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { - // if (dragX < -Config.session.dragThreshold) - // visibilities.session = true; - // else if (dragX > Config.session.dragThreshold) - // visibilities.session = false; - // - // // Show sidebar on drag if in session area and session is nearly fully visible - // if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) - // visibilities.sidebar = true; - // } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { - // // Show sidebar on drag if not in session area - // visibilities.sidebar = true; - // } } else { const outOfSidebar = x < width - panels.sidebar.width; - // Show osd on hover const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); - // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { visibilities.osd = showOsd; root.panels.osd.hovered = showOsd; } else if (showOsd) { - // If hovering over OSD area while in shortcut mode, transition to hover control osdShortcutActive = false; root.panels.osd.hovered = true; } - // - // // Show/hide session on drag - // if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { - // if (dragX < -Config.session.dragThreshold) - // visibilities.session = true; - // else if (dragX > Config.session.dragThreshold) - // visibilities.session = false; - // } - // - // // Hide sidebar on drag - // if (pressed && inRightPanel(panels.sidebar, dragStart.x, 0) && dragX > Config.sidebar.dragThreshold) - // visibilities.sidebar = false; } - // Show launcher on hover, or show/hide on drag if hover is disabled - // if (Config.launcher.showOnHover) { - // if (!visibilities.launcher && inBottomPanel(panels.launcher, x, y)) - // visibilities.launcher = true; - // } else if (pressed && inBottomPanel(panels.launcher, dragStart.x, dragStart.y) && withinPanelWidth(panels.launcher, x, y)) { - // if (dragY < -Config.launcher.dragThreshold) - // visibilities.launcher = true; - // else if (dragY > Config.launcher.dragThreshold) - // visibilities.launcher = false; - // } - // - // // Show dashboard on hover - // const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboard, x, y); - // - // // Always update visibility based on hover if not in shortcut mode - // if (!dashboardShortcutActive) { - // visibilities.dashboard = showDashboard; - // } else if (showDashboard) { - // // If hovering over dashboard area while in shortcut mode, transition to hover control - // dashboardShortcutActive = false; - // } - // - // // Show/hide dashboard on drag (for touchscreen devices) - // if (pressed && inTopPanel(panels.dashboard, dragStart.x, dragStart.y) && withinPanelWidth(panels.dashboard, x, y)) { - // if (dragY > Config.dashboard.dragThreshold) - // visibilities.dashboard = true; - // else if (dragY < -Config.dashboard.dragThreshold) - // visibilities.dashboard = false; - // } - // - // // Show utilities on hover - // const showUtilities = inBottomPanel(panels.utilities, x, y); - // - // // Always update visibility based on hover if not in shortcut mode - // if (!utilitiesShortcutActive) { - // visibilities.utilities = showUtilities; - // } else if (showUtilities) { - // // If hovering over utilities area while in shortcut mode, transition to hover control - // utilitiesShortcutActive = false; - // } - - // Show popouts on hover if (y < bar.implicitHeight) { bar.checkPopout(x); } } - // Monitor individual visibility changes Connections { function onDashboardChanged() { if (root.visibilities.dashboard) { - // Dashboard became visible, immediately check if this should be shortcut mode const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); if (!inDashboardArea) { root.dashboardShortcutActive = true; @@ -209,20 +123,21 @@ CustomMouseArea { root.visibilities.sidebar = false; root.popouts.hasCurrent = false; } else { - // Dashboard hidden, clear shortcut flag root.dashboardShortcutActive = false; - // root.visibilities.bar = false; } } + function onIsDrawingChanged() { + if (!root.visibilities.isDrawing) + root.drawing.clear(); + } + function onLauncherChanged() { - // If launcher is hidden, clear shortcut flags for dashboard and OSD if (!root.visibilities.launcher) { root.dashboardShortcutActive = false; root.osdShortcutActive = false; root.utilitiesShortcutActive = false; - // Also hide dashboard and OSD if they're not being hovered const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); if (!inOsdArea) { @@ -234,13 +149,11 @@ CustomMouseArea { function onOsdChanged() { if (root.visibilities.osd) { - // OSD became visible, immediately check if this should be shortcut mode const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); if (!inOsdArea) { root.osdShortcutActive = true; } } else { - // OSD hidden, clear shortcut flag root.osdShortcutActive = false; } } @@ -260,13 +173,11 @@ CustomMouseArea { function onUtilitiesChanged() { if (root.visibilities.utilities) { - // Utilities became visible, immediately check if this should be shortcut mode const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY); if (!inUtilitiesArea) { root.utilitiesShortcutActive = true; } } else { - // Utilities hidden, clear shortcut flag root.utilitiesShortcutActive = false; } } diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 7dea587..0c81408 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -11,6 +11,7 @@ import qs.Components.Toast as Toasts import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources import qs.Modules.Settings as Settings +import qs.Modules.Drawing as Drawing import qs.Config Item { @@ -18,6 +19,8 @@ Item { required property Item bar readonly property alias dashboard: dashboard + readonly property alias drawing: drawing + required property Canvas drawingItem readonly property alias launcher: launcher readonly property alias notifications: notifications readonly property alias osd: osd @@ -47,6 +50,16 @@ Item { visibilities: root.visibilities } + Drawing.Wrapper { + id: drawing + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + drawing: root.drawingItem + screen: root.screen + visibilities: root.visibilities + } + Osd.Wrapper { id: osd diff --git a/Modules/Drawing/Background.qml b/Modules/Drawing/Background.qml new file mode 100644 index 0000000..f19ba9a --- /dev/null +++ b/Modules/Drawing/Background.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Shapes +import qs.Components +import qs.Config + +ShapePath { + id: root + + readonly property bool flatten: wrapper.width < rounding * 2 + readonly property real rounding: Appearance.rounding.normal + readonly property real roundingX: flatten ? wrapper.width / 2 : rounding + required property Wrapper wrapper + + fillColor: DynamicColors.palette.m3surface + strokeWidth: -1 + + Behavior on fillColor { + CAnim { + } + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: root.wrapper.width - root.roundingX * 2 + relativeY: 0 + } + + PathArc { + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: 0 + relativeY: root.wrapper.height - root.rounding * 2 + } + + PathArc { + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: -root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: -(root.wrapper.width - root.roundingX * 2) + relativeY: 0 + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: -root.roundingX + relativeY: root.rounding + } +} diff --git a/Modules/Drawing/Content.qml b/Modules/Drawing/Content.qml new file mode 100644 index 0000000..4bcffa0 --- /dev/null +++ b/Modules/Drawing/Content.qml @@ -0,0 +1,76 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Config +import qs.Components + +Item { + id: root + + readonly property var colors: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"] + required property Canvas drawing + required property var visibilities + + implicitHeight: huePicker.implicitHeight + 12 + palette.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: huePicker.implicitWidth + Appearance.padding.normal * 2 + + Column { + anchors.centerIn: parent + spacing: 12 + + ColorArcPicker { + id: huePicker + + drawing: root.drawing + } + + GridLayout { + id: palette + + anchors.left: parent.left + anchors.right: parent.right + columns: 5 + rowSpacing: 8 + rows: 2 + + Repeater { + model: root.colors + + delegate: Item { + required property color modelData + readonly property bool selected: Qt.colorEqual(root.drawing.penColor, modelData) + + Layout.fillWidth: true + height: 28 + + CustomRect { + anchors.centerIn: parent + border.color: selected ? "#ffffff" : Qt.rgba(1, 1, 1, 0.28) + border.width: selected ? 3 : 1 + color: "transparent" + height: parent.height + radius: width / 2 + width: parent.height + } + + CustomRect { + anchors.centerIn: parent + border.color: Qt.rgba(0, 0, 0, 0.25) + border.width: Qt.colorEqual(modelData, "#ffffff") ? 1 : 0 + color: modelData + height: 20 + radius: width / 2 + width: 20 + } + + MouseArea { + anchors.fill: parent + + onClicked: root.drawing.penColor = parent.modelData + } + } + } + } + } +} diff --git a/Modules/Drawing/Wrapper.qml b/Modules/Drawing/Wrapper.qml new file mode 100644 index 0000000..6908fd8 --- /dev/null +++ b/Modules/Drawing/Wrapper.qml @@ -0,0 +1,133 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import qs.Components +import qs.Helpers +import qs.Config +import qs.Daemons + +Item { + id: root + + required property Canvas drawing + property bool expanded: true + required property ShellScreen screen + readonly property bool shouldBeActive: visibilities.isDrawing + required property var visibilities + + function show(): void { + visibilities.isDrawing = true; + timer.restart(); + } + + implicitHeight: content.implicitHeight + implicitWidth: 0 + visible: width > 0 + + states: [ + State { + name: "hidden" + when: !root.shouldBeActive + + PropertyChanges { + root.implicitWidth: 0 + } + + PropertyChanges { + icon.opacity: 0 + } + + PropertyChanges { + content.opacity: 0 + } + }, + State { + name: "collapsed" + when: root.shouldBeActive && !root.expanded + + PropertyChanges { + root.implicitWidth: icon.implicitWidth + } + + PropertyChanges { + icon.opacity: 1 + } + + PropertyChanges { + content.opacity: 0 + } + }, + State { + name: "visible" + when: root.shouldBeActive && root.expanded + + PropertyChanges { + root.implicitWidth: content.implicitWidth + } + + PropertyChanges { + icon.opacity: 0 + } + + PropertyChanges { + content.opacity: 1 + } + } + ] + transitions: [ + Transition { + from: "*" + to: "*" + + ParallelAnimation { + Anim { + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitWidth" + target: root + } + + Anim { + duration: Appearance.anim.durations.small + property: "opacity" + target: icon + } + + Anim { + duration: Appearance.anim.durations.small + property: "opacity" + target: content + } + } + } + ] + + Loader { + id: icon + + active: Qt.binding(() => root.shouldBeActive || root.visible) + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: content.contentItem.height + opacity: 1 + + sourceComponent: MaterialIcon { + font.pointSize: 14 + text: "arrow_forward_ios" + } + } + + Loader { + id: content + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + sourceComponent: Content { + drawing: root.drawing + visibilities: root.visibilities + } + + Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) + } +} diff --git a/Modules/Shortcuts.qml b/Modules/Shortcuts.qml index 7da6200..ced923c 100644 --- a/Modules/Shortcuts.qml +++ b/Modules/Shortcuts.qml @@ -24,6 +24,15 @@ Scope { } } + CustomShortcut { + name: "toggle-drawing" + + onPressed: { + const visibilities = Visibilities.getForActive(); + visibilities.isDrawing = !visibilities.isDrawing; + } + } + CustomShortcut { name: "toggle-nc" -- 2.47.3 From fb15c2421b049783b6b93191bcedc045a6e658e0 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 9 Mar 2026 13:05:45 +0100 Subject: [PATCH 07/47] fix screenshot picker showing up in screenshots --- Helpers/Picker.qml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Helpers/Picker.qml b/Helpers/Picker.qml index 802b4ac..39cf4f1 100644 --- a/Helpers/Picker.qml +++ b/Helpers/Picker.qml @@ -141,7 +141,7 @@ MouseArea { sy = ssy; ex = x; ey = y; - } else { + } else if (!saveTimer.running) { checkClientRects(x, y); } } @@ -154,7 +154,7 @@ MouseArea { return; if (root.loader.freeze) { - save(); + saveTimer.start(); } else { overlay.visible = border.visible = false; screencopy.visible = false; @@ -162,6 +162,16 @@ MouseArea { } } + Timer { + id: saveTimer + + interval: 25 + repeat: false + running: false + + onTriggered: root.save() + } + SequentialAnimation { id: closeAnim @@ -217,9 +227,10 @@ MouseArea { paintCursor: false onHasContentChanged: { - if (hasContent && !root.loader.freeze) { + if (hasContent) { overlay.visible = border.visible = true; - root.save(); + if (!root.loader.freeze) + root.save(); } } } @@ -233,6 +244,7 @@ MouseArea { layer.enabled: true opacity: 0.3 radius: root.realRounding + visible: false layer.effect: MultiEffect { maskEnabled: true @@ -270,6 +282,7 @@ MouseArea { implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 + visible: false x: selectionRect.x - root.realBorderWidth y: selectionRect.y - root.realBorderWidth -- 2.47.3 From 88d795a73f0b8bff3d372c2676f2c532585d4202 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 9 Mar 2026 22:17:34 +0100 Subject: [PATCH 08/47] start of refactor --- Components/CustomWindow.qml | 9 ++ Config/BarConfig.qml | 2 + Config/Config.qml | 2 + Drawers/Backgrounds.qml | 2 +- Drawers/Bar.qml | 18 +-- Drawers/Exclusions.qml | 41 +++++++ Drawers/Panels.qml | 2 +- Modules/Bar/Bar.qml | 215 ++++++++++++++++++++++++++++++++ Modules/Bar/BarLoader.qml | 239 +++++++----------------------------- Modules/Bar/Border.qml | 6 +- Modules/Drawing/Wrapper.qml | 10 +- Modules/Workspaces.qml | 4 +- 12 files changed, 329 insertions(+), 221 deletions(-) create mode 100644 Components/CustomWindow.qml create mode 100644 Drawers/Exclusions.qml create mode 100644 Modules/Bar/Bar.qml diff --git a/Components/CustomWindow.qml b/Components/CustomWindow.qml new file mode 100644 index 0000000..7e5bf2c --- /dev/null +++ b/Components/CustomWindow.qml @@ -0,0 +1,9 @@ +import Quickshell +import Quickshell.Wayland + +PanelWindow { + required property string name + + WlrLayershell.namespace: `ZShell-${name}` + color: "transparent" +} diff --git a/Config/BarConfig.qml b/Config/BarConfig.qml index 4ec7578..7b39915 100644 --- a/Config/BarConfig.qml +++ b/Config/BarConfig.qml @@ -2,6 +2,7 @@ import Quickshell.Io JsonObject { property bool autoHide: false + property int border: 8 property list entries: [ { id: "workspaces", @@ -60,6 +61,7 @@ JsonObject { enabled: true }, ] + property int height: 34 property Popouts popouts: Popouts { } property int rounding: 8 diff --git a/Config/Config.qml b/Config/Config.qml index ef5fb65..4fc63dc 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -81,6 +81,8 @@ Singleton { return { autoHide: barConfig.autoHide, rounding: barConfig.rounding, + border: barConfig.border, + height: barConfig.height, popouts: { tray: barConfig.popouts.tray, audio: barConfig.popouts.audio, diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index 2eab002..5493b82 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -22,7 +22,7 @@ Shape { required property PersistentProperties visibilities anchors.fill: parent - // anchors.margins: 8 + anchors.margins: Config.barConfig.border anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight preferredRendererType: Shape.CurveRenderer diff --git a/Drawers/Bar.qml b/Drawers/Bar.qml index be1bd99..82e46ca 100644 --- a/Drawers/Bar.qml +++ b/Drawers/Bar.qml @@ -56,21 +56,9 @@ Variants { y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height } - PanelWindow { - id: exclusionZone - - WlrLayershell.exclusionMode: Config.barConfig.autoHide ? ExclusionMode.Ignore : ExclusionMode.Auto - WlrLayershell.layer: WlrLayer.Bottom - WlrLayershell.namespace: "ZShell-Bar-Exclusion" - color: "transparent" - implicitHeight: backgroundRect.height - screen: bar.screen - - anchors { - left: true - right: true - top: true - } + Exclusions { + bar: barLoader + screen: scope.modelData } anchors { diff --git a/Drawers/Exclusions.qml b/Drawers/Exclusions.qml new file mode 100644 index 0000000..7c3603f --- /dev/null +++ b/Drawers/Exclusions.qml @@ -0,0 +1,41 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import qs.Config +import qs.Components + +Scope { + id: root + + required property Item bar + required property ShellScreen screen + + ExclusionZone { + anchors.top: true + exclusiveZone: root.bar.exclusiveZone + } + + ExclusionZone { + anchors.left: true + } + + ExclusionZone { + anchors.right: true + } + + ExclusionZone { + anchors.bottom: true + } + + component ExclusionZone: CustomWindow { + exclusiveZone: Config.barConfig.border + implicitHeight: 1 + implicitWidth: 1 + name: "Bar-Exclusion" + screen: root.screen + + mask: Region { + } + } +} diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 0c81408..1a1fce2 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -34,7 +34,7 @@ Item { required property PersistentProperties visibilities anchors.fill: parent - // anchors.margins: 8 + anchors.margins: Config.barConfig.border anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight Behavior on anchors.topMargin { diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml new file mode 100644 index 0000000..eb1dacd --- /dev/null +++ b/Modules/Bar/Bar.qml @@ -0,0 +1,215 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Modules +import qs.Config +import qs.Helpers +import qs.Modules.UPower +import qs.Modules.Network + +RowLayout { + id: root + + required property Wrapper popouts + required property ShellScreen screen + readonly property int vPadding: 6 + required property PersistentProperties visibilities + + function checkPopout(x: real): void { + const ch = childAt(x, 2) as WrappedLoader; + + if (!ch || ch?.id === "spacer") { + if (!popouts.currentName.startsWith("traymenu")) + popouts.hasCurrent = false; + return; + } + + if (visibilities.sidebar || visibilities.dashboard || visibilities.resources) + return; + + const id = ch.id; + const top = ch.x; + const item = ch.item; + const itemWidth = item.implicitWidth; + + if (id === "audio" && Config.barConfig.popouts.audio) { + popouts.currentName = "audio"; + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.hasCurrent = true; + } else if (id === "network" && Config.barConfig.popouts.network) { + popouts.currentName = "network"; + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.hasCurrent = true; + } else if (id === "upower" && Config.barConfig.popouts.upower) { + popouts.currentName = "upower"; + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.hasCurrent = true; + } + } + + spacing: Appearance.spacing.small + + Repeater { + id: repeater + + // model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index) + model: Config.barConfig.entries + + DelegateChooser { + role: "id" + + DelegateChoice { + roleValue: "spacer" + + delegate: WrappedLoader { + Layout.fillWidth: true + } + } + + DelegateChoice { + roleValue: "workspaces" + + delegate: WrappedLoader { + sourceComponent: Workspaces { + screen: root.screen + } + } + } + + DelegateChoice { + roleValue: "audio" + + delegate: WrappedLoader { + sourceComponent: AudioWidget { + } + } + } + + DelegateChoice { + roleValue: "tray" + + delegate: WrappedLoader { + sourceComponent: TrayWidget { + bar: root.bar + loader: root + popouts: root.popouts + } + } + } + + DelegateChoice { + roleValue: "resources" + + delegate: WrappedLoader { + sourceComponent: Resources { + visibilities: root.visibilities + } + } + } + + DelegateChoice { + roleValue: "updates" + + delegate: WrappedLoader { + sourceComponent: UpdatesWidget { + } + } + } + + DelegateChoice { + roleValue: "notifBell" + + delegate: WrappedLoader { + sourceComponent: NotifBell { + popouts: root.popouts + visibilities: root.visibilities + } + } + } + + DelegateChoice { + roleValue: "clock" + + delegate: WrappedLoader { + sourceComponent: Clock { + loader: root + popouts: root.popouts + visibilities: root.visibilities + } + } + } + + DelegateChoice { + roleValue: "activeWindow" + + delegate: WrappedLoader { + sourceComponent: WindowTitle { + bar: root + } + } + } + + DelegateChoice { + roleValue: "upower" + + delegate: WrappedLoader { + sourceComponent: UPowerWidget { + } + } + } + + DelegateChoice { + roleValue: "network" + + delegate: WrappedLoader { + sourceComponent: NetworkWidget { + } + } + } + + DelegateChoice { + roleValue: "media" + + delegate: WrappedLoader { + sourceComponent: MediaWidget { + } + } + } + } + } + + component WrappedLoader: Loader { + required property bool enabled + required property string id + required property int index + + function findFirstEnabled(): Item { + const count = repeater.count; + for (let i = 0; i < count; i++) { + const item = repeater.itemAt(i); + if (item?.enabled) + return item; + } + return null; + } + + function findLastEnabled(): Item { + for (let i = repeater.count - 1; i >= 0; i--) { + const item = repeater.itemAt(i); + if (item?.enabled) + return item; + } + return null; + } + + Layout.alignment: Qt.AlignVCenter + Layout.fillHeight: true + Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0 + Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0 + active: enabled + visible: enabled + } +} diff --git a/Modules/Bar/BarLoader.qml b/Modules/Bar/BarLoader.qml index 67e27e5..560529e 100644 --- a/Modules/Bar/BarLoader.qml +++ b/Modules/Bar/BarLoader.qml @@ -11,223 +11,72 @@ import qs.Helpers import qs.Modules.UPower import qs.Modules.Network -RowLayout { +Item { id: root required property PanelWindow bar + readonly property int contentHeight: Config.barConfig.height + padding * 2 + readonly property int exclusiveZone: Config.barConfig.autoHide ? Config.barConfig.border : contentHeight + property bool isHovered + readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border) required property Wrapper popouts required property ShellScreen screen + readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar) readonly property int vPadding: 6 required property PersistentProperties visibilities function checkPopout(x: real): void { - const ch = childAt(x, 2) as WrappedLoader; - - if (!ch || ch?.id === "spacer") { - if (!popouts.currentName.startsWith("traymenu")) - popouts.hasCurrent = false; - return; - } - - if (visibilities.sidebar || visibilities.dashboard || visibilities.resources) - return; - - const id = ch.id; - const top = ch.x; - const item = ch.item; - const itemWidth = item.implicitWidth; - - if (id === "audio" && Config.barConfig.popouts.audio) { - popouts.currentName = "audio"; - popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); - popouts.hasCurrent = true; - } else if (id === "network" && Config.barConfig.popouts.network) { - popouts.currentName = "network"; - popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); - popouts.hasCurrent = true; - } else if (id === "upower" && Config.barConfig.popouts.upower) { - popouts.currentName = "upower"; - popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); - popouts.hasCurrent = true; - } + content.item?.checkPopout(x); } - implicitHeight: 34 + implicitHeight: Config.barConfig.border + visible: width > Config.barConfig.border - CustomShortcut { - name: "toggle-overview" + states: State { + name: "visible" + when: root.shouldBeVisible - onPressed: { - Hyprland.refreshWorkspaces(); - Hyprland.refreshMonitors(); - if (root.popouts.hasCurrent && root.popouts.currentName === "overview") { - root.popouts.hasCurrent = false; - } else { - root.popouts.currentName = "overview"; - root.popouts.currentCenter = root.width / 2; - root.popouts.hasCurrent = true; - } + PropertyChanges { + root.implicitHeight: root.contentHeight } } + transitions: [ + Transition { + from: "" + to: "visible" - Repeater { - id: repeater - - // model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index) - model: Config.barConfig.entries - - DelegateChooser { - role: "id" - - DelegateChoice { - roleValue: "spacer" - - delegate: WrappedLoader { - Layout.fillWidth: true - } + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + property: "implicitHeight" + target: root } + }, + Transition { + from: "visible" + to: "" - DelegateChoice { - roleValue: "workspaces" - - delegate: WrappedLoader { - sourceComponent: Workspaces { - bar: root.bar - } - } - } - - DelegateChoice { - roleValue: "audio" - - delegate: WrappedLoader { - sourceComponent: AudioWidget { - } - } - } - - DelegateChoice { - roleValue: "tray" - - delegate: WrappedLoader { - sourceComponent: TrayWidget { - bar: root.bar - loader: root - popouts: root.popouts - } - } - } - - DelegateChoice { - roleValue: "resources" - - delegate: WrappedLoader { - sourceComponent: Resources { - visibilities: root.visibilities - } - } - } - - DelegateChoice { - roleValue: "updates" - - delegate: WrappedLoader { - sourceComponent: UpdatesWidget { - } - } - } - - DelegateChoice { - roleValue: "notifBell" - - delegate: WrappedLoader { - sourceComponent: NotifBell { - popouts: root.popouts - visibilities: root.visibilities - } - } - } - - DelegateChoice { - roleValue: "clock" - - delegate: WrappedLoader { - sourceComponent: Clock { - loader: root - popouts: root.popouts - visibilities: root.visibilities - } - } - } - - DelegateChoice { - roleValue: "activeWindow" - - delegate: WrappedLoader { - sourceComponent: WindowTitle { - bar: root - } - } - } - - DelegateChoice { - roleValue: "upower" - - delegate: WrappedLoader { - sourceComponent: UPowerWidget { - } - } - } - - DelegateChoice { - roleValue: "network" - - delegate: WrappedLoader { - sourceComponent: NetworkWidget { - } - } - } - - DelegateChoice { - roleValue: "media" - - delegate: WrappedLoader { - sourceComponent: MediaWidget { - } - } + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasized + property: "implicitHeight" + target: root } } - } + ] - component WrappedLoader: Loader { - required property bool enabled - required property string id - required property int index + Loader { + id: content - function findFirstEnabled(): Item { - const count = repeater.count; - for (let i = 0; i < count; i++) { - const item = repeater.itemAt(i); - if (item?.enabled) - return item; - } - return null; + active: root.shouldBeVisible || root.visible + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + sourceComponent: Bar { + height: root.contentHeight + popouts: root.popouts + screen: root.screen + visibilities: root.visibilities } - - function findLastEnabled(): Item { - for (let i = repeater.count - 1; i >= 0; i--) { - const item = repeater.itemAt(i); - if (item?.enabled) - return item; - } - return null; - } - - Layout.alignment: Qt.AlignVCenter - Layout.fillHeight: true - Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0 - Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0 - active: enabled - visible: enabled } } diff --git a/Modules/Bar/Border.qml b/Modules/Bar/Border.qml index 875d218..c091795 100644 --- a/Modules/Bar/Border.qml +++ b/Modules/Bar/Border.qml @@ -38,9 +38,11 @@ Item { Rectangle { anchors.fill: parent + anchors.margins: Config.barConfig.border anchors.topMargin: Config.barConfig.autoHide && !root.visibilities.bar ? 4 : root.bar.implicitHeight - topLeftRadius: 8 - topRightRadius: 8 + radius: Config.barConfig.border > 0 ? Config.barConfig.rounding : 0 + topLeftRadius: Config.barConfig.rounding + topRightRadius: Config.barConfig.rounding Behavior on anchors.topMargin { Anim { diff --git a/Modules/Drawing/Wrapper.qml b/Modules/Drawing/Wrapper.qml index 6908fd8..2705e61 100644 --- a/Modules/Drawing/Wrapper.qml +++ b/Modules/Drawing/Wrapper.qml @@ -16,11 +16,6 @@ Item { readonly property bool shouldBeActive: visibilities.isDrawing required property var visibilities - function show(): void { - visibilities.isDrawing = true; - timer.restart(); - } - implicitHeight: content.implicitHeight implicitWidth: 0 visible: width > 0 @@ -102,6 +97,11 @@ Item { } ] + onVisibleChanged: { + if (!visible) + root.expanded = true; + } + Loader { id: icon diff --git a/Modules/Workspaces.qml b/Modules/Workspaces.qml index e444508..9fd47dc 100644 --- a/Modules/Workspaces.qml +++ b/Modules/Workspaces.qml @@ -12,9 +12,9 @@ Item { id: root property real activeWorkspaceMargin: Math.ceil(Appearance.padding.small / 2) - required property PanelWindow bar readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1 - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.bar.screen) + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + required property ShellScreen screen property int workspaceButtonWidth: bgRect.implicitHeight - root.activeWorkspaceMargin * 2 property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown readonly property list workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor) -- 2.47.3 From 26bc5cd7c3bafcb649ad6b198ba404bb749aec63 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 9 Mar 2026 23:18:13 +0100 Subject: [PATCH 09/47] start of refactor --- Drawers/Interactions.qml | 6 +++--- Modules/Bar/Bar.qml | 23 +++++++++++------------ Modules/Dashboard/Wrapper.qml | 22 +++++++++++++--------- Modules/Osd/Wrapper.qml | 32 ++++++++++++++++++-------------- Modules/TrayItem.qml | 19 ------------------- Modules/TrayWidget.qml | 2 -- 6 files changed, 45 insertions(+), 59 deletions(-) diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 79b0f72..fe03599 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -20,15 +20,15 @@ CustomMouseArea { required property PersistentProperties visibilities function inBottomPanel(panel: Item, x: real, y: real): bool { - return y > root.height - panel.height && withinPanelWidth(panel, x, y); + return y > root.height - panel.height - Config.barConfig.border && withinPanelWidth(panel, x, y); } function inLeftPanel(panel: Item, x: real, y: real): bool { - return x < panel.x + panel.width && withinPanelHeight(panel, x, y); + return x < panel.x + panel.width + Config.barConfig.border && withinPanelHeight(panel, x, y); } function inRightPanel(panel: Item, x: real, y: real): bool { - return x > panel.x && withinPanelHeight(panel, x, y); + return x > panel.x - Config.barConfig.border && withinPanelHeight(panel, x, y); } function inTopPanel(panel: Item, x: real, y: real): bool { diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index eb1dacd..3b58312 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -4,7 +4,7 @@ import Quickshell import QtQuick import QtQuick.Layouts import qs.Components -import qs.Modules +import qs.Modules as Bar import qs.Config import qs.Helpers import qs.Modules.UPower @@ -13,7 +13,7 @@ import qs.Modules.Network RowLayout { id: root - required property Wrapper popouts + required property Bar.Wrapper popouts required property ShellScreen screen readonly property int vPadding: 6 required property PersistentProperties visibilities @@ -73,7 +73,7 @@ RowLayout { roleValue: "workspaces" delegate: WrappedLoader { - sourceComponent: Workspaces { + sourceComponent: Bar.Workspaces { screen: root.screen } } @@ -83,7 +83,7 @@ RowLayout { roleValue: "audio" delegate: WrappedLoader { - sourceComponent: AudioWidget { + sourceComponent: Bar.AudioWidget { } } } @@ -92,8 +92,7 @@ RowLayout { roleValue: "tray" delegate: WrappedLoader { - sourceComponent: TrayWidget { - bar: root.bar + sourceComponent: Bar.TrayWidget { loader: root popouts: root.popouts } @@ -104,7 +103,7 @@ RowLayout { roleValue: "resources" delegate: WrappedLoader { - sourceComponent: Resources { + sourceComponent: Bar.Resources { visibilities: root.visibilities } } @@ -114,7 +113,7 @@ RowLayout { roleValue: "updates" delegate: WrappedLoader { - sourceComponent: UpdatesWidget { + sourceComponent: Bar.UpdatesWidget { } } } @@ -123,7 +122,7 @@ RowLayout { roleValue: "notifBell" delegate: WrappedLoader { - sourceComponent: NotifBell { + sourceComponent: Bar.NotifBell { popouts: root.popouts visibilities: root.visibilities } @@ -134,7 +133,7 @@ RowLayout { roleValue: "clock" delegate: WrappedLoader { - sourceComponent: Clock { + sourceComponent: Bar.Clock { loader: root popouts: root.popouts visibilities: root.visibilities @@ -146,7 +145,7 @@ RowLayout { roleValue: "activeWindow" delegate: WrappedLoader { - sourceComponent: WindowTitle { + sourceComponent: Bar.WindowTitle { bar: root } } @@ -174,7 +173,7 @@ RowLayout { roleValue: "media" delegate: WrappedLoader { - sourceComponent: MediaWidget { + sourceComponent: Bar.MediaWidget { } } } diff --git a/Modules/Dashboard/Wrapper.qml b/Modules/Dashboard/Wrapper.qml index 930b8a6..11858d6 100644 --- a/Modules/Dashboard/Wrapper.qml +++ b/Modules/Dashboard/Wrapper.qml @@ -72,17 +72,21 @@ Item { } } - Loader { - id: content + CustomClippingRect { + anchors.fill: parent - active: true - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - visible: false + Loader { + id: content - sourceComponent: Content { - state: root.dashState - visibilities: root.visibilities + active: true + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + visible: false + + sourceComponent: Content { + state: root.dashState + visibilities: root.visibilities + } } } } diff --git a/Modules/Osd/Wrapper.qml b/Modules/Osd/Wrapper.qml index 55f0311..b0815e8 100644 --- a/Modules/Osd/Wrapper.qml +++ b/Modules/Osd/Wrapper.qml @@ -113,22 +113,26 @@ Item { } } - Loader { - id: content + CustomClippingRect { + anchors.fill: parent - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter + Loader { + id: content - sourceComponent: Content { - brightness: root.brightness - monitor: root.monitor - muted: root.muted - sourceMuted: root.sourceMuted - sourceVolume: root.sourceVolume - visibilities: root.visibilities - volume: root.volume + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + sourceComponent: Content { + brightness: root.brightness + monitor: root.monitor + muted: root.muted + sourceMuted: root.sourceMuted + sourceVolume: root.sourceVolume + visibilities: root.visibilities + volume: root.volume + } + + Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) } - - Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) } } diff --git a/Modules/TrayItem.qml b/Modules/TrayItem.qml index c613677..9580969 100644 --- a/Modules/TrayItem.qml +++ b/Modules/TrayItem.qml @@ -9,7 +9,6 @@ import qs.Config Item { id: root - required property PanelWindow bar property bool hasLoaded: false required property int ind required property SystemTrayItem item @@ -46,22 +45,4 @@ Item { layer.enabled: DynamicColors.light source: root.item.icon } - - // Image { - // id: icon - // - // property bool batteryHDPI: root.bar.screen.x < 0 && root.item.icon.includes("battery") - // property bool nmHDPI: root.bar.screen.x < 0 && root.item.icon.includes("nm-") - // - // anchors.centerIn: parent - // width: batteryHDPI ? 26 : ( nmHDPI ? 25 : 22 ) - // height: batteryHDPI ? 26 : ( nmHDPI ? 25 : 22 ) - // source: root.item.icon - // mipmap: true - // smooth: ( batteryHDPI || nmHDPI ) ? false : true - // asynchronous: true - // sourceSize.width: ( batteryHDPI || nmHDPI ) ? 16 : 22 - // sourceSize.height: ( batteryHDPI || nmHDPI ) ? 16 : 22 - // fillMode: Image.PreserveAspectFit - // } } diff --git a/Modules/TrayWidget.qml b/Modules/TrayWidget.qml index 07e2e02..fd55ed8 100644 --- a/Modules/TrayWidget.qml +++ b/Modules/TrayWidget.qml @@ -10,7 +10,6 @@ import qs.Config Item { id: root - required property PanelWindow bar readonly property alias items: repeater required property RowLayout loader required property Wrapper popouts @@ -45,7 +44,6 @@ Item { required property int index required property SystemTrayItem modelData - bar: root.bar implicitHeight: 34 implicitWidth: 34 ind: index -- 2.47.3 From 098db5e9031c01037fb9ab3d8608b791dbf332d3 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Tue, 10 Mar 2026 15:39:29 +0100 Subject: [PATCH 10/47] refactor done? --- Components/CustomRadioButton.qml | 2 +- Components/MaterialIcon.qml | 2 +- Config/AppearanceConf.qml | 1 + Drawers/Backgrounds.qml | 7 +- Drawers/Interactions.qml | 12 +- Drawers/Panels.qml | 7 +- Drawers/{Bar.qml => Windows.qml} | 84 ++++++------- Modules/AudioWidget.qml | 24 ++-- Modules/Bar/Bar.qml | 3 +- Modules/Bar/BarLoader.qml | 14 +-- Modules/Bar/Border.qml | 9 +- Modules/Calendar/CalendarHeader.qml | 73 ------------ Modules/Calendar/CalendarPopup.qml | 77 ------------ Modules/Calendar/DayOfWeekRow.qml | 42 ------- Modules/Calendar/MonthGrid.qml | 118 ------------------- Modules/Calendar/WeekNumberColumn.qml | 45 ------- Modules/Clock.qml | 47 ++++---- Modules/Content.qml | 9 -- Modules/Drawing/Wrapper.qml | 2 +- Modules/MediaWidget.qml | 24 +--- Modules/NotifBell.qml | 53 ++++----- Modules/Notifications/Sidebar/Background.qml | 2 +- Modules/Resource-old.qml | 116 ------------------ Modules/ResourceDetail.qml | 77 ------------ Modules/ResourcePopout-old.qml | 59 ---------- Modules/Resources.qml | 25 +--- Modules/TrayMenuPopout.qml | 2 + Modules/TrayWidget.qml | 51 ++++---- Modules/UPower/UPowerPopout.qml | 2 +- Modules/UPower/UPowerWidget.qml | 17 +-- Modules/UpdatesWidget.qml | 22 ++-- Modules/WindowTitle.qml | 2 +- Modules/Workspaces.qml | 6 +- shell.qml | 2 +- 34 files changed, 161 insertions(+), 877 deletions(-) rename Drawers/{Bar.qml => Windows.qml} (75%) delete mode 100644 Modules/Calendar/CalendarHeader.qml delete mode 100644 Modules/Calendar/CalendarPopup.qml delete mode 100644 Modules/Calendar/DayOfWeekRow.qml delete mode 100644 Modules/Calendar/MonthGrid.qml delete mode 100644 Modules/Calendar/WeekNumberColumn.qml delete mode 100644 Modules/Resource-old.qml delete mode 100644 Modules/ResourceDetail.qml delete mode 100644 Modules/ResourcePopout-old.qml diff --git a/Components/CustomRadioButton.qml b/Components/CustomRadioButton.qml index f719261..7e743cb 100644 --- a/Components/CustomRadioButton.qml +++ b/Components/CustomRadioButton.qml @@ -5,7 +5,7 @@ import qs.Config RadioButton { id: root - font.pointSize: 12 + font.pointSize: Appearance.font.size.normal implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin diff --git a/Components/MaterialIcon.qml b/Components/MaterialIcon.qml index 031c6a7..69da4e3 100644 --- a/Components/MaterialIcon.qml +++ b/Components/MaterialIcon.qml @@ -5,7 +5,7 @@ CustomText { property int grade: DynamicColors.light ? 0 : -25 font.family: "Material Symbols Rounded" - font.pointSize: 15 + font.pointSize: Appearance.font.size.larger font.variableAxes: ({ FILL: fill.toFixed(1), GRAD: grade, diff --git a/Config/AppearanceConf.qml b/Config/AppearanceConf.qml index c76b05e..93defb5 100644 --- a/Config/AppearanceConf.qml +++ b/Config/AppearanceConf.qml @@ -71,6 +71,7 @@ JsonObject { property real scale: 1 property int small: 5 * scale property int smaller: 7 * scale + property int smallest: 2 * scale } component Rounding: JsonObject { property int full: 1000 * scale diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index 5493b82..c7abccc 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -23,14 +23,9 @@ Shape { anchors.fill: parent anchors.margins: Config.barConfig.border - anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight + anchors.topMargin: bar.implicitHeight preferredRendererType: Shape.CurveRenderer - Behavior on anchors.topMargin { - Anim { - } - } - Drawing.Background { startX: 0 startY: wrapper.y - rounding diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index fe03599..25f3077 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -66,8 +66,8 @@ CustomMouseArea { popouts.hasCurrent = false; } - if (Config.barConfig.autoHide && !root.visibilities.sidebar && !root.visibilities.dashboard) - root.visibilities.bar = false; + if (Config.barConfig.autoHide) + bar.isHovered = false; } } onPositionChanged: event => { @@ -84,8 +84,8 @@ CustomMouseArea { root.panels.drawing.expanded = false; } - if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight + bar.anchors.topMargin) - visibilities.bar = true; + if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight) + bar.isHovered = true; if (panels.sidebar.width === 0) { const showOsd = inRightPanel(panels.osd, x, y); @@ -107,8 +107,8 @@ CustomMouseArea { } } - if (y < bar.implicitHeight) { - bar.checkPopout(x); + if (y < root.bar.implicitHeight) { + root.bar.checkPopout(x); } } diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 1a1fce2..32b64f9 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -35,12 +35,7 @@ Item { anchors.fill: parent anchors.margins: Config.barConfig.border - anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight - - Behavior on anchors.topMargin { - Anim { - } - } + anchors.topMargin: bar.implicitHeight Resources.Wrapper { id: resources diff --git a/Drawers/Bar.qml b/Drawers/Windows.qml similarity index 75% rename from Drawers/Bar.qml rename to Drawers/Windows.qml index 82e46ca..a47e4a5 100644 --- a/Drawers/Bar.qml +++ b/Drawers/Windows.qml @@ -21,18 +21,23 @@ Variants { required property var modelData - PanelWindow { - id: bar + Exclusions { + bar: bar + screen: scope.modelData + } + CustomWindow { + id: win + + readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) property var root: Quickshell.shellDir - property bool trayMenuVisible: false WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - WlrLayershell.namespace: "ZShell-Bar" color: "transparent" contentItem.focus: true mask: visibilities.isDrawing ? null : region + name: "Bar" screen: scope.modelData contentItem.Keys.onEscapePressed: { @@ -44,21 +49,23 @@ Variants { visibilities.settings = false; visibilities.resources = false; } + onHasFullscreenChanged: { + visibilities.launcher = false; + visibilities.dashboard = false; + visibilities.osd = false; + visibilities.settings = false; + visibilities.resources = false; + } Region { id: region - height: bar.screen.height - backgroundRect.implicitHeight + height: win.height - bar.implicitHeight - Config.barConfig.border intersection: Intersection.Xor regions: popoutRegions.instances - width: bar.width - x: 0 - y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height - } - - Exclusions { - bar: barLoader - screen: scope.modelData + width: win.width - Config.barConfig.border * 2 + x: Config.barConfig.border + y: bar.implicitHeight } anchors { @@ -79,8 +86,8 @@ Variants { height: modelData.height intersection: Intersection.Subtract width: modelData.width - x: modelData.x - y: modelData.y + backgroundRect.implicitHeight + x: modelData.x + Config.barConfig.border + y: modelData.y + bar.implicitHeight } } @@ -88,7 +95,7 @@ Variants { id: focusGrab active: visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) - windows: [bar] + windows: [win] onCleared: { visibilities.launcher = false; @@ -136,12 +143,12 @@ Variants { } Border { - bar: backgroundRect + bar: bar visibilities: visibilities } Backgrounds { - bar: backgroundRect + bar: bar panels: panels visibilities: visibilities } @@ -157,7 +164,7 @@ Variants { DrawingInput { id: input - bar: backgroundRect + bar: bar drawing: drawing panels: panels popout: panels.drawing @@ -169,7 +176,7 @@ Variants { id: mouseArea anchors.fill: parent - bar: barLoader + bar: bar drawing: drawing input: input panels: panels @@ -181,45 +188,20 @@ Variants { Panels { id: panels - bar: backgroundRect + bar: bar drawingItem: drawing screen: scope.modelData visibilities: visibilities } - CustomRect { - id: backgroundRect - - property Wrapper popouts: panels.popouts + BarLoader { + id: bar anchors.left: parent.left anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? -30 : 0 - color: "transparent" - implicitHeight: barLoader.implicitHeight - radius: 0 - - Behavior on anchors.topMargin { - Anim { - } - } - Behavior on color { - CAnim { - } - } - - BarLoader { - id: barLoader - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - bar: bar - popouts: panels.popouts - screen: scope.modelData - visibilities: visibilities - } + popouts: panels.popouts + screen: scope.modelData + visibilities: visibilities } } } diff --git a/Modules/AudioWidget.qml b/Modules/AudioWidget.qml index d9031cb..a4d0185 100644 --- a/Modules/AudioWidget.qml +++ b/Modules/AudioWidget.qml @@ -7,15 +7,16 @@ import qs.Modules import qs.Config import qs.Components -Item { +CustomRect { id: root property color barColor: DynamicColors.palette.m3primary property color textColor: DynamicColors.palette.m3onSurface - anchors.bottom: parent.bottom - anchors.top: parent.top + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitWidth: 150 + radius: Appearance.rounding.full Behavior on implicitWidth { NumberAnimation { @@ -24,28 +25,21 @@ Item { } } - CustomRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: height / 2 - } + Component.onCompleted: console.log(root.height, root.implicitHeight) RowLayout { id: layout - anchors.fill: parent + anchors.left: parent.left anchors.leftMargin: Appearance.padding.small - anchors.rightMargin: Appearance.padding.small * 2 anchors.verticalCenter: parent.verticalCenter + width: root.implicitWidth - Appearance.padding.small * 3 MaterialIcon { Layout.alignment: Qt.AlignVCenter animate: true color: Audio.muted ? DynamicColors.palette.m3error : root.textColor - font.pointSize: 14 + font.pointSize: Appearance.font.size.normal text: Audio.muted ? "volume_off" : "volume_up" } @@ -74,7 +68,7 @@ Item { Layout.alignment: Qt.AlignVCenter animate: true color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor - font.pointSize: 14 + font.pointSize: Appearance.font.size.normal text: Audio.sourceMuted ? "mic_off" : "mic" } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 3b58312..72dfbb8 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -19,7 +19,7 @@ RowLayout { required property PersistentProperties visibilities function checkPopout(x: real): void { - const ch = childAt(x, 2) as WrappedLoader; + const ch = childAt(x, height / 2) as WrappedLoader; if (!ch || ch?.id === "spacer") { if (!popouts.currentName.startsWith("traymenu")) @@ -205,7 +205,6 @@ RowLayout { } Layout.alignment: Qt.AlignVCenter - Layout.fillHeight: true Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0 Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0 active: enabled diff --git a/Modules/Bar/BarLoader.qml b/Modules/Bar/BarLoader.qml index 560529e..0e4863f 100644 --- a/Modules/Bar/BarLoader.qml +++ b/Modules/Bar/BarLoader.qml @@ -14,14 +14,13 @@ import qs.Modules.Network Item { id: root - required property PanelWindow bar readonly property int contentHeight: Config.barConfig.height + padding * 2 readonly property int exclusiveZone: Config.barConfig.autoHide ? Config.barConfig.border : contentHeight property bool isHovered readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border) required property Wrapper popouts required property ShellScreen screen - readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar) + readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar || isHovered) readonly property int vPadding: 6 required property PersistentProperties visibilities @@ -30,7 +29,7 @@ Item { } implicitHeight: Config.barConfig.border - visible: width > Config.barConfig.border + visible: height > Config.barConfig.border states: State { name: "visible" @@ -46,8 +45,8 @@ Item { to: "visible" Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects property: "implicitHeight" target: root } @@ -57,7 +56,8 @@ Item { to: "" Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects property: "implicitHeight" target: root } @@ -68,9 +68,9 @@ Item { id: content active: root.shouldBeVisible || root.visible + anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - anchors.top: parent.top sourceComponent: Bar { height: root.contentHeight diff --git a/Modules/Bar/Border.qml b/Modules/Bar/Border.qml index c091795..f82496f 100644 --- a/Modules/Bar/Border.qml +++ b/Modules/Bar/Border.qml @@ -17,7 +17,7 @@ Item { CustomRect { anchors.fill: parent - color: Config.barConfig.autoHide && !root.visibilities.bar ? "transparent" : DynamicColors.palette.m3surface + color: Config.barConfig.border === 1 ? "transparent" : DynamicColors.palette.m3surface layer.enabled: true layer.effect: MultiEffect { @@ -39,15 +39,10 @@ Item { Rectangle { anchors.fill: parent anchors.margins: Config.barConfig.border - anchors.topMargin: Config.barConfig.autoHide && !root.visibilities.bar ? 4 : root.bar.implicitHeight + anchors.topMargin: root.bar.implicitHeight radius: Config.barConfig.border > 0 ? Config.barConfig.rounding : 0 topLeftRadius: Config.barConfig.rounding topRightRadius: Config.barConfig.rounding - - Behavior on anchors.topMargin { - Anim { - } - } } } } diff --git a/Modules/Calendar/CalendarHeader.qml b/Modules/Calendar/CalendarHeader.qml deleted file mode 100644 index bc7b158..0000000 --- a/Modules/Calendar/CalendarHeader.qml +++ /dev/null @@ -1,73 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -RowLayout { - spacing: 12 - - Rectangle { - Layout.preferredHeight: 40 - Layout.preferredWidth: 40 - color: "transparent" - radius: 1000 - - MaterialIcon { - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurface - fill: 1 - font.pointSize: 24 - text: "arrow_back_2" - } - - StateLayer { - onClicked: { - if (Calendar.displayMonth === 0) { - Calendar.displayMonth = 11; - Calendar.displayYear -= 1; - } else { - Calendar.displayMonth -= 1; - } - } - } - } - - CustomText { - Layout.fillWidth: true - color: DynamicColors.palette.m3onSurface - font.pointSize: 14 - font.weight: 600 - horizontalAlignment: Text.AlignHCenter - text: new Date(Calendar.displayYear, Calendar.displayMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy") - } - - Rectangle { - Layout.preferredHeight: 40 - Layout.preferredWidth: 40 - color: "transparent" - radius: 1000 - - MaterialIcon { - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurface - fill: 1 - font.pointSize: 24 - rotation: 180 - text: "arrow_back_2" - } - - StateLayer { - onClicked: { - if (Calendar.displayMonth === 11) { - Calendar.displayMonth = 0; - Calendar.displayYear += 1; - } else { - Calendar.displayMonth += 1; - } - } - } - } -} diff --git a/Modules/Calendar/CalendarPopup.qml b/Modules/Calendar/CalendarPopup.qml deleted file mode 100644 index 0dcf110..0000000 --- a/Modules/Calendar/CalendarPopup.qml +++ /dev/null @@ -1,77 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -Item { - id: root - - required property Item wrapper - - implicitHeight: layout.childrenRect.height + layout.anchors.margins * 2 - implicitWidth: layout.childrenRect.width + layout.anchors.margins * 2 - - ColumnLayout { - id: layout - - anchors.centerIn: parent - anchors.margins: 16 - spacing: 16 - - // Header with month/year and navigation - CalendarHeader { - Layout.fillWidth: true - Layout.preferredHeight: childrenRect.height - } - - // Calendar grid - RowLayout { - Layout.fillWidth: true - Layout.preferredHeight: childrenRect.height - spacing: 12 - - ColumnLayout { - Layout.alignment: Qt.AlignTop - Layout.preferredHeight: childrenRect.height - Layout.preferredWidth: weekNumberColumn.width - spacing: 8 - - Item { - Layout.preferredHeight: dayOfWeekRow.height - } - - WeekNumberColumn { - id: weekNumberColumn - - Layout.alignment: Qt.AlignTop - Layout.preferredHeight: weekNumbers.values.length * 44 - } - } - - ColumnLayout { - Layout.alignment: Qt.AlignTop - Layout.fillWidth: true - Layout.preferredHeight: childrenRect.height - spacing: 8 - - DayOfWeekRow { - id: dayOfWeekRow - - Layout.fillWidth: true - Layout.preferredHeight: 30 - locale: Qt.locale() - } - - MonthGrid { - Layout.preferredHeight: childrenRect.height - Layout.preferredWidth: childrenRect.width - locale: Qt.locale() - wrapper: root.wrapper - } - } - } - } -} diff --git a/Modules/Calendar/DayOfWeekRow.qml b/Modules/Calendar/DayOfWeekRow.qml deleted file mode 100644 index 10fa64c..0000000 --- a/Modules/Calendar/DayOfWeekRow.qml +++ /dev/null @@ -1,42 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -RowLayout { - id: root - - required property var locale - - spacing: 4 - - Repeater { - model: 7 - - Item { - readonly property string dayName: { - // Get the day name for this column - const dayIndex = (index + Calendar.weekStartDay) % 7; - return root.locale.dayName(dayIndex, Locale.ShortFormat); - } - required property int index - - Layout.fillWidth: true - Layout.preferredHeight: 30 - - CustomText { - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 11 - font.weight: 500 - horizontalAlignment: Text.AlignHCenter - opacity: 0.8 - text: parent.dayName - verticalAlignment: Text.AlignVCenter - } - } - } -} diff --git a/Modules/Calendar/MonthGrid.qml b/Modules/Calendar/MonthGrid.qml deleted file mode 100644 index 5ab370b..0000000 --- a/Modules/Calendar/MonthGrid.qml +++ /dev/null @@ -1,118 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -GridLayout { - id: root - - required property var locale - required property Item wrapper - - columnSpacing: 4 - columns: 7 - rowSpacing: 4 - uniformCellHeights: true - uniformCellWidths: true - - Repeater { - id: repeater - - model: ScriptModel { - values: Calendar.getWeeksForMonth(Calendar.displayMonth, Calendar.displayYear) - - Behavior on values { - SequentialAnimation { - id: switchAnim - - ParallelAnimation { - Anim { - from: 1.0 - property: "opacity" - to: 0.0 - } - - Anim { - from: 1.0 - property: "scale" - to: 0.8 - } - } - - PropertyAction { - } - - ParallelAnimation { - Anim { - from: 0.0 - property: "opacity" - to: 1.0 - } - - Anim { - from: 0.8 - property: "scale" - to: 1.0 - } - } - } - } - } - - Rectangle { - required property int index - required property var modelData - - Layout.preferredHeight: width - Layout.preferredWidth: 40 - color: { - if (modelData.isToday) { - return DynamicColors.palette.m3primaryContainer; - } - return "transparent"; - } - radius: 1000 - - Behavior on color { - ColorAnimation { - duration: 200 - } - } - - CustomText { - anchors.centerIn: parent - color: { - if (parent.modelData.isToday) { - return DynamicColors.palette.m3onPrimaryContainer; - } - return DynamicColors.palette.m3onSurface; - } - horizontalAlignment: Text.AlignHCenter - opacity: parent.modelData.isCurrentMonth ? 1.0 : 0.4 - text: parent.modelData.day.toString() - verticalAlignment: Text.AlignVCenter - - Behavior on color { - ColorAnimation { - duration: 200 - } - } - Behavior on opacity { - NumberAnimation { - duration: 200 - } - } - } - } - } - - component Anim: NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - target: root - } -} diff --git a/Modules/Calendar/WeekNumberColumn.qml b/Modules/Calendar/WeekNumberColumn.qml deleted file mode 100644 index 44e30b6..0000000 --- a/Modules/Calendar/WeekNumberColumn.qml +++ /dev/null @@ -1,45 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -ColumnLayout { - id: root - - readonly property var weekNumbers: Calendar.getWeekNumbers(Calendar.displayMonth, Calendar.displayYear) - - spacing: 4 - - Repeater { - model: ScriptModel { - values: root.weekNumbers - } - - Item { - id: weekItem - - required property int index - required property var modelData - - Layout.alignment: Qt.AlignHCenter - Layout.preferredHeight: 40 - Layout.preferredWidth: 20 - - CustomText { - id: weekText - - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 - horizontalAlignment: Text.AlignHCenter - opacity: 0.5 - text: weekItem.modelData - verticalAlignment: Text.AlignVCenter - } - } - } -} diff --git a/Modules/Clock.qml b/Modules/Clock.qml index 9598bc7..78dd270 100644 --- a/Modules/Clock.qml +++ b/Modules/Clock.qml @@ -6,43 +6,36 @@ import qs.Modules import qs.Helpers as Helpers import qs.Components -Item { +CustomRect { id: root required property RowLayout loader required property Wrapper popouts required property PersistentProperties visibilities - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitWidth: timeText.contentWidth + 5 * 2 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 + implicitWidth: timeText.contentWidth + Appearance.padding.normal * 2 + radius: Appearance.rounding.full - CustomRect { - anchors.bottomMargin: 3 - anchors.fill: parent - anchors.topMargin: 3 - color: "transparent" - radius: 4 + CustomText { + id: timeText - CustomText { - id: timeText + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurface + text: Time.dateStr - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurface - text: Time.dateStr - - Behavior on color { - CAnim { - } - } - } - - StateLayer { - acceptedButtons: Qt.LeftButton - - onClicked: { - root.visibilities.dashboard = !root.visibilities.dashboard; + Behavior on color { + CAnim { } } } + + StateLayer { + acceptedButtons: Qt.LeftButton + + onClicked: { + root.visibilities.dashboard = !root.visibilities.dashboard; + } + } } diff --git a/Modules/Content.qml b/Modules/Content.qml index 80e16d0..54a1f45 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -5,7 +5,6 @@ import Quickshell.Services.SystemTray import QtQuick import qs.Config import qs.Components -import qs.Modules.Calendar import qs.Modules.WSOverview import qs.Modules.Network import qs.Modules.UPower @@ -69,14 +68,6 @@ Item { } } - Popout { - name: "calendar" - - sourceComponent: CalendarPopup { - wrapper: root.wrapper - } - } - Popout { name: "overview" diff --git a/Modules/Drawing/Wrapper.qml b/Modules/Drawing/Wrapper.qml index 2705e61..a2df74d 100644 --- a/Modules/Drawing/Wrapper.qml +++ b/Modules/Drawing/Wrapper.qml @@ -112,7 +112,7 @@ Item { opacity: 1 sourceComponent: MaterialIcon { - font.pointSize: 14 + font.pointSize: Appearance.font.size.larger text: "arrow_forward_ios" } } diff --git a/Modules/MediaWidget.qml b/Modules/MediaWidget.qml index cbb67d2..6979b43 100644 --- a/Modules/MediaWidget.qml +++ b/Modules/MediaWidget.qml @@ -5,30 +5,22 @@ import qs.Daemons import qs.Config import qs.Helpers -Item { +CustomRect { id: root readonly property string currentMedia: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") readonly property int textWidth: Math.min(metrics.width, 200) - anchors.bottom: parent.bottom - anchors.top: parent.top + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 + radius: Appearance.rounding.full Behavior on implicitWidth { Anim { } } - CustomRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: Appearance.rounding.full - } - TextMetrics { id: metrics @@ -39,11 +31,7 @@ Item { RowLayout { id: layout - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.left: parent.left - anchors.leftMargin: Appearance.padding.normal - anchors.top: parent.top + anchors.centerIn: parent Behavior on implicitWidth { Anim { @@ -53,7 +41,7 @@ Item { MaterialIcon { animate: true color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface - font.pointSize: 14 + font.pointSize: Appearance.font.size.normal text: Players.active?.isPlaying ? "music_note" : "music_off" } diff --git a/Modules/NotifBell.qml b/Modules/NotifBell.qml index ebe9746..7ddd506 100644 --- a/Modules/NotifBell.qml +++ b/Modules/NotifBell.qml @@ -5,46 +5,39 @@ import qs.Config import qs.Helpers import qs.Components -Item { +CustomRect { id: root required property Wrapper popouts required property PersistentProperties visibilities - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitWidth: 30 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 + implicitWidth: implicitHeight + radius: Appearance.rounding.full - CustomRect { - anchors.bottomMargin: 3 - anchors.fill: parent - anchors.topMargin: 3 - color: "transparent" - radius: 4 + MaterialIcon { + id: notificationCenterIcon - MaterialIcon { - id: notificationCenterIcon + property color iconColor: DynamicColors.palette.m3onSurface - property color iconColor: DynamicColors.palette.m3onSurface + anchors.centerIn: parent + color: iconColor + font.family: "Material Symbols Rounded" + font.pointSize: Appearance.font.size.larger + text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4" - anchors.centerIn: parent - color: iconColor - font.family: "Material Symbols Rounded" - font.pointSize: 16 - text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4" - - Behavior on color { - CAnim { - } - } - } - - StateLayer { - cursorShape: Qt.PointingHandCursor - - onClicked: { - root.visibilities.sidebar = !root.visibilities.sidebar; + Behavior on color { + CAnim { } } } + + StateLayer { + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.visibilities.sidebar = !root.visibilities.sidebar; + } + } } diff --git a/Modules/Notifications/Sidebar/Background.qml b/Modules/Notifications/Sidebar/Background.qml index c41693b..bf15baf 100644 --- a/Modules/Notifications/Sidebar/Background.qml +++ b/Modules/Notifications/Sidebar/Background.qml @@ -15,7 +15,7 @@ ShapePath { readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width required property Wrapper wrapper - fillColor: flatten ? "transparent" : DynamicColors.palette.m3surface + fillColor: DynamicColors.palette.m3surface strokeWidth: -1 Behavior on fillColor { diff --git a/Modules/Resource-old.qml b/Modules/Resource-old.qml deleted file mode 100644 index 3be26f9..0000000 --- a/Modules/Resource-old.qml +++ /dev/null @@ -1,116 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes -import qs.Components -import qs.Config - -Item { - id: root - - property color borderColor: warning ? DynamicColors.palette.m3onError : mainColor - required property color mainColor - required property double percentage - property bool shown: true - property color usageColor: warning ? DynamicColors.palette.m3error : mainColor - property bool warning: percentage * 100 >= warningThreshold - property int warningThreshold: 100 - - clip: true - implicitHeight: 22 - implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth - visible: width > 0 && height > 0 - - Behavior on percentage { - NumberAnimation { - duration: 300 - easing.type: Easing.InOutQuad - } - } - - RowLayout { - id: resourceRowLayout - - spacing: 2 - x: shown ? 0 : -resourceRowLayout.width - - anchors { - verticalCenter: parent.verticalCenter - } - - Item { - Layout.alignment: Qt.AlignVCenter - implicitHeight: root.implicitHeight - implicitWidth: 14 - - Rectangle { - id: backgroundCircle - - anchors.centerIn: parent - border.color: "#404040" - border.width: 1 - color: "#40000000" - height: 14 - radius: height / 2 - width: 14 - } - - Shape { - anchors.fill: backgroundCircle - preferredRendererType: Shape.CurveRenderer - smooth: true - - ShapePath { - fillColor: root.usageColor - startX: backgroundCircle.width / 2 - startY: backgroundCircle.height / 2 - strokeWidth: 0 - - Behavior on fillColor { - CAnim { - } - } - - PathLine { - x: backgroundCircle.width / 2 - y: 0 + (1 / 2) - } - - PathAngleArc { - centerX: backgroundCircle.width / 2 - centerY: backgroundCircle.height / 2 - radiusX: backgroundCircle.width / 2 - (1 / 2) - radiusY: backgroundCircle.height / 2 - (1 / 2) - startAngle: -90 - sweepAngle: 360 * root.percentage - } - - PathLine { - x: backgroundCircle.width / 2 - y: backgroundCircle.height / 2 - } - } - - ShapePath { - capStyle: ShapePath.FlatCap - fillColor: "transparent" - strokeColor: root.borderColor - strokeWidth: 1 - - Behavior on strokeColor { - CAnim { - } - } - - PathAngleArc { - centerX: backgroundCircle.width / 2 - centerY: backgroundCircle.height / 2 - radiusX: backgroundCircle.width / 2 - (1 / 2) - radiusY: backgroundCircle.height / 2 - (1 / 2) - startAngle: -90 - sweepAngle: 360 * root.percentage - } - } - } - } - } -} diff --git a/Modules/ResourceDetail.qml b/Modules/ResourceDetail.qml deleted file mode 100644 index 1ec97f5..0000000 --- a/Modules/ResourceDetail.qml +++ /dev/null @@ -1,77 +0,0 @@ -import Quickshell -import QtQuick -import QtQuick.Layouts -import qs.Config -import qs.Components - -Item { - id: root - - property color barColor: DynamicColors.palette.m3primary - required property string details - required property string iconString - required property double percentage - required property string resourceName - property color textColor: DynamicColors.palette.m3onSurface - property color warningBarColor: DynamicColors.palette.m3error - required property int warningThreshold - - Layout.preferredHeight: columnLayout.implicitHeight - Layout.preferredWidth: 158 - - ColumnLayout { - id: columnLayout - - anchors.fill: parent - spacing: 4 - - Row { - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - spacing: 6 - - MaterialIcon { - color: root.textColor - font.family: "Material Symbols Rounded" - font.pointSize: 28 - text: root.iconString - } - - CustomText { - anchors.verticalCenter: parent.verticalCenter - color: root.textColor - font.pointSize: 12 - text: root.resourceName - } - } - - Rectangle { - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.preferredHeight: 6 - color: "#40000000" - radius: height / 2 - - Rectangle { - color: root.percentage * 100 >= root.warningThreshold ? root.warningBarColor : root.barColor - height: parent.height - radius: height / 2 - width: parent.width * Math.min(root.percentage, 1) - - Behavior on width { - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - } - } - - CustomText { - Layout.alignment: Qt.AlignLeft - color: root.textColor - font.pointSize: 10 - text: root.details - } - } -} diff --git a/Modules/ResourcePopout-old.qml b/Modules/ResourcePopout-old.qml deleted file mode 100644 index 4dcc35c..0000000 --- a/Modules/ResourcePopout-old.qml +++ /dev/null @@ -1,59 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import qs.Config - -Item { - id: popoutWindow - - required property var wrapper - - implicitHeight: contentColumn.implicitHeight + 10 - implicitWidth: contentColumn.implicitWidth + 10 * 2 - - // ShadowRect { - // anchors.fill: contentRect - // radius: 8 - // } - - ColumnLayout { - id: contentColumn - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 10 - - ResourceDetail { - details: qsTr("%1 of %2 MB used").arg(Math.round(ResourceUsage.memoryUsed * 0.001)).arg(Math.round(ResourceUsage.memoryTotal * 0.001)) - iconString: "\uf7a3" - percentage: ResourceUsage.memoryUsedPercentage - resourceName: qsTr("Memory Usage") - warningThreshold: 95 - } - - ResourceDetail { - details: qsTr("%1% used").arg(Math.round(ResourceUsage.cpuUsage * 100)) - iconString: "\ue322" - percentage: ResourceUsage.cpuUsage - resourceName: qsTr("CPU Usage") - warningThreshold: 95 - } - - ResourceDetail { - details: qsTr("%1% used").arg(Math.round(ResourceUsage.gpuUsage * 100)) - iconString: "\ue30f" - percentage: ResourceUsage.gpuUsage - resourceName: qsTr("GPU Usage") - warningThreshold: 95 - } - - ResourceDetail { - details: qsTr("%1% used").arg(Math.round(ResourceUsage.gpuMemUsage * 100)) - iconString: "\ue30d" - percentage: ResourceUsage.gpuMemUsage - resourceName: qsTr("VRAM Usage") - warningThreshold: 95 - } - } -} diff --git a/Modules/Resources.qml b/Modules/Resources.qml index ab36c71..66742c3 100644 --- a/Modules/Resources.qml +++ b/Modules/Resources.qml @@ -8,32 +8,19 @@ import qs.Modules import qs.Config import qs.Components -Item { +CustomRect { id: root required property PersistentProperties visibilities - anchors.bottom: parent.bottom - anchors.top: parent.top clip: true + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitWidth: rowLayout.implicitWidth + Appearance.padding.small * 2 + radius: height / 2 - CustomRect { - id: backgroundRect - - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: height / 2 - - anchors { - left: parent.left - right: parent.right - verticalCenter: parent.verticalCenter - } - - StateLayer { - onClicked: root.visibilities.resources = !root.visibilities.resources - } + StateLayer { + onClicked: root.visibilities.resources = !root.visibilities.resources } RowLayout { diff --git a/Modules/TrayMenuPopout.qml b/Modules/TrayMenuPopout.qml index 7115d9b..709c8c0 100644 --- a/Modules/TrayMenuPopout.qml +++ b/Modules/TrayMenuPopout.qml @@ -194,6 +194,8 @@ StackView { } Loader { + id: loader + active: menu.isSubMenu asynchronous: true diff --git a/Modules/TrayWidget.qml b/Modules/TrayWidget.qml index fd55ed8..9f1cb5b 100644 --- a/Modules/TrayWidget.qml +++ b/Modules/TrayWidget.qml @@ -7,50 +7,41 @@ import Quickshell.Services.SystemTray import qs.Components import qs.Config -Item { +CustomClippingRect { id: root readonly property alias items: repeater required property RowLayout loader required property Wrapper popouts - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitHeight: 34 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitWidth: row.width + Appearance.padding.small * 2 + radius: height / 2 - CustomClippingRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: height / 2 + Row { + id: row - Row { - id: row + anchors.centerIn: parent + spacing: 0 - anchors.centerIn: parent - spacing: 0 + Repeater { + id: repeater - Repeater { - id: repeater + model: SystemTray.items - model: SystemTray.items + TrayItem { + id: trayItem - TrayItem { - id: trayItem + required property int index + required property SystemTrayItem modelData - required property int index - required property SystemTrayItem modelData - - implicitHeight: 34 - implicitWidth: 34 - ind: index - item: modelData - loader: root.loader - popouts: root.popouts - } + implicitHeight: 34 + implicitWidth: 34 + ind: index + item: modelData + loader: root.loader + popouts: root.popouts } } } diff --git a/Modules/UPower/UPowerPopout.qml b/Modules/UPower/UPowerPopout.qml index 8ab4b16..c60c2d1 100644 --- a/Modules/UPower/UPowerPopout.qml +++ b/Modules/UPower/UPowerPopout.qml @@ -167,7 +167,7 @@ Item { anchors.centerIn: parent color: profiles.current === text ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface fill: profiles.current === text ? 1 : 0 - font.pointSize: 36 + font.pointSize: Appearance.font.size.large * 2 text: parent.icon Behavior on fill { diff --git a/Modules/UPower/UPowerWidget.qml b/Modules/UPower/UPowerWidget.qml index beec824..d93a956 100644 --- a/Modules/UPower/UPowerWidget.qml +++ b/Modules/UPower/UPowerWidget.qml @@ -5,20 +5,13 @@ import qs.Components import qs.Config import qs.Helpers as Helpers -Item { +CustomRect { id: root - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitWidth: layout.childrenRect.width + 10 * 2 - - CustomRect { - anchors.bottomMargin: 4 - anchors.fill: parent - anchors.topMargin: 4 - color: DynamicColors.tPalette.m3surfaceContainer - radius: 1000 - } + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 + implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 + radius: Appearance.rounding.full RowLayout { id: layout diff --git a/Modules/UpdatesWidget.qml b/Modules/UpdatesWidget.qml index 13bdc54..e6166b7 100644 --- a/Modules/UpdatesWidget.qml +++ b/Modules/UpdatesWidget.qml @@ -4,24 +4,16 @@ import qs.Components import qs.Modules import qs.Config -Item { +CustomRect { id: root property int countUpdates: Updates.availableUpdates property color textColor: DynamicColors.palette.m3onSurface - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitWidth: contentRow.childrenRect.width + Appearance.spacing.smaller - - CustomRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: height / 2 - } + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 + implicitWidth: contentRow.implicitWidth + Appearance.spacing.smaller + radius: height / 2 RowLayout { id: contentRow @@ -30,13 +22,13 @@ Item { spacing: Appearance.spacing.small MaterialIcon { - font.pointSize: 14 + font.pointSize: Appearance.font.size.normal text: "package_2" } CustomText { color: root.textColor - font.pointSize: 12 + font.pointSize: Appearance.font.size.normal text: root.countUpdates } } diff --git a/Modules/WindowTitle.qml b/Modules/WindowTitle.qml index c5c1a3b..88c4cc0 100644 --- a/Modules/WindowTitle.qml +++ b/Modules/WindowTitle.qml @@ -47,7 +47,7 @@ Item { elide: Qt.ElideRight elideWidth: root.maxWidth font.family: "Rubik" - font.pointSize: 12 + font.pointSize: Appearance.font.size.normal text: Hypr.activeToplevel?.title ?? qsTr("Desktop") onElideWidthChanged: root.current.text = elidedText diff --git a/Modules/Workspaces.qml b/Modules/Workspaces.qml index 9fd47dc..1cc64e6 100644 --- a/Modules/Workspaces.qml +++ b/Modules/Workspaces.qml @@ -20,8 +20,8 @@ Item { readonly property list workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor) readonly property int workspacesShown: workspaces.length - anchors.bottom: parent.bottom - anchors.top: parent.top + height: implicitHeight + implicitHeight: Config.barConfig.height + Math.max(Appearance.padding.smaller, Config.barConfig.border) * 2 implicitWidth: (root.workspaceButtonWidth * root.workspacesShown) + root.activeWorkspaceMargin * 2 Behavior on implicitWidth { @@ -36,7 +36,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) + implicitHeight: root.implicitHeight - ((Appearance.padding.small - 1) * 2) radius: height / 2 CustomRect { diff --git a/shell.qml b/shell.qml index 85b9c7c..be777b6 100644 --- a/shell.qml +++ b/shell.qml @@ -11,7 +11,7 @@ import qs.Helpers import qs.Modules.Polkit ShellRoot { - Bar { + Windows { } Wallpaper { -- 2.47.3 From 203b19ce454978c9f54b3135c0a86dfeacf8e2d8 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Tue, 10 Mar 2026 19:15:26 +0100 Subject: [PATCH 11/47] shader hotfix --- Modules/Workspaces.qml | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/Modules/Workspaces.qml b/Modules/Workspaces.qml index 1cc64e6..9f30f02 100644 --- a/Modules/Workspaces.qml +++ b/Modules/Workspaces.qml @@ -91,7 +91,7 @@ Item { CustomText { anchors.centerIn: parent - color: DynamicColors.palette.m3onSecondaryContainer + color: button.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondaryContainer elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter text: button.modelData.name @@ -149,21 +149,11 @@ Item { } } - ShaderEffectSource { - id: activeTextTex - - anchors.fill: bgRect - anchors.margins: root.activeWorkspaceMargin - hideSource: true - live: true - recursive: true - sourceItem: activeTextSource - } - Item { id: indicatorMask anchors.fill: bgRect + layer.enabled: true visible: false CustomRect { @@ -176,21 +166,12 @@ Item { } } - ShaderEffectSource { - id: indicatorMaskEffect - - anchors.fill: activeTextSource - live: true - sourceItem: indicatorMask - visible: false - } - MultiEffect { anchors.fill: activeTextSource maskEnabled: true maskInverted: false - maskSource: indicatorMaskEffect - source: activeTextTex + maskSource: indicatorMask + source: activeTextSource z: 5 } } -- 2.47.3 From 0375491dc84a5ebeeed8633057eb1f6888db7817 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Tue, 10 Mar 2026 22:42:06 +0100 Subject: [PATCH 12/47] wtf is happening --- Helpers/SystemUsage.qml | 35 ++++++++++- Modules/AudioWidget.qml | 4 +- Modules/Resource.qml | 111 +++++++++++++++++++--------------- Modules/ResourceUsage.qml | 123 -------------------------------------- 4 files changed, 98 insertions(+), 175 deletions(-) delete mode 100644 Modules/ResourceUsage.qml diff --git a/Helpers/SystemUsage.qml b/Helpers/SystemUsage.qml index d9234e9..eb7802a 100644 --- a/Helpers/SystemUsage.qml +++ b/Helpers/SystemUsage.qml @@ -80,8 +80,9 @@ Singleton { onTriggered: { stat.reload(); meminfo.reload(); - storage.running = true; - gpuUsage.running = true; + storage.running = false; + if (root.gpuName === "GENERIC") + gpuUsage.running = true; sensors.running = true; } } @@ -274,13 +275,41 @@ Singleton { } } + Process { + id: gpuUsageNvidia + + command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"] + running: true + + stderr: SplitParser { + onRead: data => { + console.log("nvidia-smi stderr:", String(data).trim()); + } + } + stdout: SplitParser { + onRead: data => { + const [usage, temp, mem] = String(data).trim().split(/\s*,\s*/); + + root.gpuPerc = parseInt(usage, 10) / 100; + root.gpuTemp = parseInt(temp, 10); + root.gpuMemUsed = parseInt(mem, 10) / root.gpuMemTotal; + } + } + + onExited: (exitCode, exitStatus) => { + console.log("gpuUsageNvidia exited:", exitCode, exitStatus); + } + onStarted: console.log("gpuUsageNvidia started, pid =", processId) + } + Process { id: gpuUsage - command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits"] : ["echo"] + command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : ["echo"] stdout: StdioCollector { onStreamFinished: { + console.log("this is running"); if (root.gpuType === "GENERIC") { const percs = text.trim().split("\n"); const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); diff --git a/Modules/AudioWidget.qml b/Modules/AudioWidget.qml index a4d0185..64c4f46 100644 --- a/Modules/AudioWidget.qml +++ b/Modules/AudioWidget.qml @@ -39,7 +39,7 @@ CustomRect { Layout.alignment: Qt.AlignVCenter animate: true color: Audio.muted ? DynamicColors.palette.m3error : root.textColor - font.pointSize: Appearance.font.size.normal + font.pointSize: Appearance.font.size.larger text: Audio.muted ? "volume_off" : "volume_up" } @@ -68,7 +68,7 @@ CustomRect { Layout.alignment: Qt.AlignVCenter animate: true color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor - font.pointSize: Appearance.font.size.normal + font.pointSize: Appearance.font.size.larger text: Audio.sourceMuted ? "mic_off" : "mic" } diff --git a/Modules/Resource.qml b/Modules/Resource.qml index e26085c..323aebc 100644 --- a/Modules/Resource.qml +++ b/Modules/Resource.qml @@ -36,58 +36,75 @@ Item { Component.onCompleted: animatedPercentage = percentage onPercentageChanged: animatedPercentage = percentage - Canvas { - id: gaugeCanvas - - anchors.centerIn: parent - height: width - width: Math.min(parent.width, parent.height) - - Component.onCompleted: requestPaint() - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); - const cx = width / 2; - const cy = (height / 2) + 1; - const radius = (Math.min(width, height) - 12) / 2; - const lineWidth = 3; - ctx.beginPath(); - ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); - ctx.stroke(); - if (root.animatedPercentage > 0) { - ctx.beginPath(); - ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = root.accentColor; - ctx.stroke(); - } - } - - Connections { - function onAnimatedPercentageChanged() { - gaugeCanvas.requestPaint(); - } - - target: root - } - - Connections { - function onPaletteChanged() { - gaugeCanvas.requestPaint(); - } - - target: DynamicColors - } - } + // Canvas { + // id: gaugeCanvas + // + // anchors.centerIn: parent + // height: width + // width: Math.min(parent.width, parent.height) + // + // Component.onCompleted: requestPaint() + // onPaint: { + // const ctx = getContext("2d"); + // ctx.reset(); + // const cx = width / 2; + // const cy = (height / 2) + 1; + // const radius = (Math.min(width, height) - 12) / 2; + // const lineWidth = 3; + // ctx.beginPath(); + // ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); + // ctx.lineWidth = lineWidth; + // ctx.lineCap = "round"; + // ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); + // ctx.stroke(); + // if (root.animatedPercentage > 0) { + // ctx.beginPath(); + // ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage); + // ctx.lineWidth = lineWidth; + // ctx.lineCap = "round"; + // ctx.strokeStyle = root.accentColor; + // ctx.stroke(); + // } + // } + // + // Connections { + // function onAnimatedPercentageChanged() { + // gaugeCanvas.requestPaint(); + // } + // + // target: root + // } + // + // Connections { + // function onPaletteChanged() { + // gaugeCanvas.requestPaint(); + // } + // + // target: DynamicColors + // } + // } MaterialIcon { + id: icon + anchors.centerIn: parent color: DynamicColors.palette.m3onSurface font.pointSize: 12 text: root.icon } + + CustomRect { + anchors.left: icon.right + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + implicitHeight: parent.height + implicitWidth: 5 + radius: Appearance.rounding.full + + CustomRect { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + color: root.mainColor + } + } } diff --git a/Modules/ResourceUsage.qml b/Modules/ResourceUsage.qml deleted file mode 100644 index f1b01ce..0000000 --- a/Modules/ResourceUsage.qml +++ /dev/null @@ -1,123 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Config - -Singleton { - id: root - - property string autoGpuType: "NONE" - property double cpuUsage: 0 - property double gpuMemUsage: 0 - readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType - property double gpuUsage: 0 - property double memoryFree: 1 - property double memoryTotal: 1 - property double memoryUsed: memoryTotal - memoryFree - property double memoryUsedPercentage: memoryUsed / memoryTotal - property var previousCpuStats - property double swapFree: 1 - property double swapTotal: 1 - property double swapUsed: swapTotal - swapFree - property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0 - property double totalMem: 0 - - Timer { - interval: 1 - repeat: true - running: true - - onTriggered: { - // Reload files - fileMeminfo.reload(); - fileStat.reload(); - - // Parse memory and swap usage - const textMeminfo = fileMeminfo.text(); - memoryTotal = Number(textMeminfo.match(/MemTotal: *(\d+)/)?.[1] ?? 1); - memoryFree = Number(textMeminfo.match(/MemAvailable: *(\d+)/)?.[1] ?? 0); - swapTotal = Number(textMeminfo.match(/SwapTotal: *(\d+)/)?.[1] ?? 1); - swapFree = Number(textMeminfo.match(/SwapFree: *(\d+)/)?.[1] ?? 0); - - // Parse CPU usage - const textStat = fileStat.text(); - const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); - if (cpuLine) { - const stats = cpuLine.slice(1).map(Number); - const total = stats.reduce((a, b) => a + b, 0); - const idle = stats[3]; - - if (previousCpuStats) { - const totalDiff = total - previousCpuStats.total; - const idleDiff = idle - previousCpuStats.idle; - cpuUsage = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; - } - - previousCpuStats = { - total, - idle - }; - } - if (root.gpuType === "NVIDIA") { - processGpu.running = true; - } - - interval = 3000; - } - } - - FileView { - id: fileMeminfo - - path: "/proc/meminfo" - } - - FileView { - id: fileStat - - path: "/proc/stat" - } - - Process { - id: gpuTypeCheck - - command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] - running: !Config.services.gpuType - - stdout: StdioCollector { - onStreamFinished: root.autoGpuType = text.trim() - } - } - - Process { - id: oneshotMem - - command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"] - running: root.gpuType === "NVIDIA" && totalMem === 0 - - stdout: StdioCollector { - onStreamFinished: { - totalMem = Number(this.text.trim()); - oneshotMem.running = false; - } - } - } - - Process { - id: processGpu - - command: ["nvidia-smi", "--query-gpu=utilization.gpu,memory.used", "--format=csv,noheader,nounits"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - const parts = this.text.trim().split(", "); - gpuUsage = Number(parts[0]) / 100; - gpuMemUsage = Number(parts[1]) / totalMem; - } - } - } -} -- 2.47.3 From bab596850bd30c3aa24214101a07f07caf186a8b Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Wed, 11 Mar 2026 11:22:22 +0100 Subject: [PATCH 13/47] slight changes, gpu usage down by 20% --- Helpers/SystemUsage.qml | 76 +++++++++++++++++---------- Modules/Resource.qml | 111 +++++++++++++++++----------------------- shell.qml | 2 +- 3 files changed, 97 insertions(+), 92 deletions(-) diff --git a/Helpers/SystemUsage.qml b/Helpers/SystemUsage.qml index eb7802a..5ced856 100644 --- a/Helpers/SystemUsage.qml +++ b/Helpers/SystemUsage.qml @@ -17,7 +17,6 @@ Singleton { property var disks: [] property real gpuMemTotal: 0 property real gpuMemUsed - property string gpuName: "" property real gpuPerc property real gpuTemp readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType @@ -80,9 +79,19 @@ Singleton { onTriggered: { stat.reload(); meminfo.reload(); - storage.running = false; - if (root.gpuName === "GENERIC") + storage.running = true; + if (root.gpuType === "GENERIC") gpuUsage.running = true; + } + } + + Timer { + interval: Config.dashboard.resourceUpdateInterval * 5 + repeat: true + running: root.refCount > 0 + triggeredOnStart: true + + onTriggered: { sensors.running = true; } } @@ -113,10 +122,13 @@ Singleton { const totalDiff = total - root.lastCpuTotal; const idleDiff = idle - root.lastCpuIdle; - root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; + const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; root.lastCpuTotal = total; root.lastCpuIdle = idle; + + if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01) + root.cpuPerc = newCpuPerc; } } } @@ -128,8 +140,14 @@ Singleton { onLoaded: { const data = text(); - root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; - root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; + const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; + const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; + + if (root.memTotal !== total) + root.memTotal = total; + + if (Math.abs(used - root.memUsed) >= 16384) + root.memUsed = used; } } @@ -279,27 +297,36 @@ Singleton { id: gpuUsageNvidia command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"] - running: true + running: root.refCount > 0 && root.gpuType === "NVIDIA" - stderr: SplitParser { - onRead: data => { - console.log("nvidia-smi stderr:", String(data).trim()); - } - } stdout: SplitParser { onRead: data => { - const [usage, temp, mem] = String(data).trim().split(/\s*,\s*/); + const parts = String(data).trim().split(/\s*,\s*/); + if (parts.length < 3) + return; - root.gpuPerc = parseInt(usage, 10) / 100; - root.gpuTemp = parseInt(temp, 10); - root.gpuMemUsed = parseInt(mem, 10) / root.gpuMemTotal; + const usageRaw = parseInt(parts[0], 10); + const tempRaw = parseInt(parts[1], 10); + const memRaw = parseInt(parts[2], 10); + + if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw)) + return; + + const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100)); + const newGpuTemp = tempRaw; + const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0; + + // Only publish meaningful changes to avoid needless binding churn / repaints + if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01) + root.gpuPerc = newGpuPerc; + + if (Math.abs(root.gpuTemp - newGpuTemp) >= 1) + root.gpuTemp = newGpuTemp; + + if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01) + root.gpuMemUsed = newGpuMemUsed; } } - - onExited: (exitCode, exitStatus) => { - console.log("gpuUsageNvidia exited:", exitCode, exitStatus); - } - onStarted: console.log("gpuUsageNvidia started, pid =", processId) } Process { @@ -314,11 +341,6 @@ Singleton { const percs = text.trim().split("\n"); const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); root.gpuPerc = sum / percs.length / 100; - } else if (root.gpuType === "NVIDIA") { - const [usage, temp, mem] = text.trim().split(","); - root.gpuPerc = parseInt(usage, 10) / 100; - root.gpuTemp = parseInt(temp, 10); - root.gpuMemUsed = parseInt(mem, 10) / root.gpuMemTotal; } else { root.gpuPerc = 0; root.gpuTemp = 0; @@ -343,7 +365,7 @@ Singleton { // If AMD Tdie pattern failed, try fallback on Tctl cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/); - if (cpuTemp) + if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5) root.cpuTemp = parseFloat(cpuTemp[1]); if (root.gpuType !== "GENERIC") diff --git a/Modules/Resource.qml b/Modules/Resource.qml index 323aebc..e26085c 100644 --- a/Modules/Resource.qml +++ b/Modules/Resource.qml @@ -36,75 +36,58 @@ Item { Component.onCompleted: animatedPercentage = percentage onPercentageChanged: animatedPercentage = percentage - // Canvas { - // id: gaugeCanvas - // - // anchors.centerIn: parent - // height: width - // width: Math.min(parent.width, parent.height) - // - // Component.onCompleted: requestPaint() - // onPaint: { - // const ctx = getContext("2d"); - // ctx.reset(); - // const cx = width / 2; - // const cy = (height / 2) + 1; - // const radius = (Math.min(width, height) - 12) / 2; - // const lineWidth = 3; - // ctx.beginPath(); - // ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); - // ctx.lineWidth = lineWidth; - // ctx.lineCap = "round"; - // ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); - // ctx.stroke(); - // if (root.animatedPercentage > 0) { - // ctx.beginPath(); - // ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage); - // ctx.lineWidth = lineWidth; - // ctx.lineCap = "round"; - // ctx.strokeStyle = root.accentColor; - // ctx.stroke(); - // } - // } - // - // Connections { - // function onAnimatedPercentageChanged() { - // gaugeCanvas.requestPaint(); - // } - // - // target: root - // } - // - // Connections { - // function onPaletteChanged() { - // gaugeCanvas.requestPaint(); - // } - // - // target: DynamicColors - // } - // } + Canvas { + id: gaugeCanvas + + anchors.centerIn: parent + height: width + width: Math.min(parent.width, parent.height) + + Component.onCompleted: requestPaint() + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + const cx = width / 2; + const cy = (height / 2) + 1; + const radius = (Math.min(width, height) - 12) / 2; + const lineWidth = 3; + ctx.beginPath(); + ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); + ctx.stroke(); + if (root.animatedPercentage > 0) { + ctx.beginPath(); + ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage); + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = root.accentColor; + ctx.stroke(); + } + } + + Connections { + function onAnimatedPercentageChanged() { + gaugeCanvas.requestPaint(); + } + + target: root + } + + Connections { + function onPaletteChanged() { + gaugeCanvas.requestPaint(); + } + + target: DynamicColors + } + } MaterialIcon { - id: icon - anchors.centerIn: parent color: DynamicColors.palette.m3onSurface font.pointSize: 12 text: root.icon } - - CustomRect { - anchors.left: icon.right - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) - implicitHeight: parent.height - implicitWidth: 5 - radius: Appearance.rounding.full - - CustomRect { - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - color: root.mainColor - } - } } diff --git a/shell.qml b/shell.qml index be777b6..16d49f5 100644 --- a/shell.qml +++ b/shell.qml @@ -1,6 +1,6 @@ //@ pragma UseQApplication //@ pragma Env QSG_RENDER_LOOP=threaded -//@ pragma Env QSG_USE_SIMPLE_ANIMATION_DRIVER=1 +//@ pragma Env QSG_USE_SIMPLE_ANIMATION_DRIVER=0 //@ pragma Env QS_NO_RELOAD_POPUP=1 import Quickshell import qs.Modules -- 2.47.3 From 646605cc9b6b3f364453a0b573c1333f098fc122 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Wed, 11 Mar 2026 19:47:14 +0100 Subject: [PATCH 14/47] slight changes --- Helpers/SystemUsage.qml | 12 +++- Modules/Resource.qml | 125 +++++++++++++++++++++++----------------- Modules/Resources.qml | 9 ++- 3 files changed, 89 insertions(+), 57 deletions(-) diff --git a/Helpers/SystemUsage.qml b/Helpers/SystemUsage.qml index 5ced856..e613060 100644 --- a/Helpers/SystemUsage.qml +++ b/Helpers/SystemUsage.qml @@ -79,12 +79,22 @@ Singleton { onTriggered: { stat.reload(); meminfo.reload(); - storage.running = true; if (root.gpuType === "GENERIC") gpuUsage.running = true; } } + Timer { + interval: 60000 * 120 + repeat: true + running: true + triggeredOnStart: true + + onTriggered: { + storage.running = true; + } + } + Timer { interval: Config.dashboard.resourceUpdateInterval * 5 repeat: true diff --git a/Modules/Resource.qml b/Modules/Resource.qml index e26085c..89b16a4 100644 --- a/Modules/Resource.qml +++ b/Modules/Resource.qml @@ -4,7 +4,7 @@ import QtQuick.Shapes import qs.Components import qs.Config -Item { +RowLayout { id: root property color accentColor: warning ? DynamicColors.palette.m3error : mainColor @@ -20,12 +20,7 @@ Item { property int warningThreshold: 80 clip: true - height: implicitHeight - implicitHeight: 34 - implicitWidth: 34 percentage: 0 - visible: width > 0 && height > 0 - width: implicitWidth Behavior on animatedPercentage { Anim { @@ -36,58 +31,80 @@ Item { Component.onCompleted: animatedPercentage = percentage onPercentageChanged: animatedPercentage = percentage - Canvas { - id: gaugeCanvas - - anchors.centerIn: parent - height: width - width: Math.min(parent.width, parent.height) - - Component.onCompleted: requestPaint() - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); - const cx = width / 2; - const cy = (height / 2) + 1; - const radius = (Math.min(width, height) - 12) / 2; - const lineWidth = 3; - ctx.beginPath(); - ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); - ctx.stroke(); - if (root.animatedPercentage > 0) { - ctx.beginPath(); - ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = root.accentColor; - ctx.stroke(); - } - } - - Connections { - function onAnimatedPercentageChanged() { - gaugeCanvas.requestPaint(); - } - - target: root - } - - Connections { - function onPaletteChanged() { - gaugeCanvas.requestPaint(); - } - - target: DynamicColors - } - } + // Canvas { + // id: gaugeCanvas + // + // anchors.centerIn: parent + // height: width + // width: Math.min(parent.width, parent.height) + // + // Component.onCompleted: requestPaint() + // onPaint: { + // const ctx = getContext("2d"); + // ctx.reset(); + // const cx = width / 2; + // const cy = (height / 2) + 1; + // const radius = (Math.min(width, height) - 12) / 2; + // const lineWidth = 3; + // ctx.beginPath(); + // ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); + // ctx.lineWidth = lineWidth; + // ctx.lineCap = "round"; + // ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); + // ctx.stroke(); + // if (root.animatedPercentage > 0) { + // ctx.beginPath(); + // ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage); + // ctx.lineWidth = lineWidth; + // ctx.lineCap = "round"; + // ctx.strokeStyle = root.accentColor; + // ctx.stroke(); + // } + // } + // + // Connections { + // function onAnimatedPercentageChanged() { + // gaugeCanvas.requestPaint(); + // } + // + // target: root + // } + // + // Connections { + // function onPaletteChanged() { + // gaugeCanvas.requestPaint(); + // } + // + // target: DynamicColors + // } + // } MaterialIcon { - anchors.centerIn: parent + id: icon + color: DynamicColors.palette.m3onSurface font.pointSize: 12 text: root.icon } + + CustomClippingRect { + Layout.preferredHeight: root.height + Layout.preferredWidth: 5 + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + radius: Appearance.rounding.full + + CustomRect { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + color: root.mainColor + implicitHeight: root.percentage * parent.height + radius: implicitHeight / 2 + + Behavior on implicitHeight { + Anim { + } + } + } + } } diff --git a/Modules/Resources.qml b/Modules/Resources.qml index 66742c3..e90100a 100644 --- a/Modules/Resources.qml +++ b/Modules/Resources.qml @@ -16,7 +16,7 @@ CustomRect { clip: true color: DynamicColors.tPalette.m3surfaceContainer implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 - implicitWidth: rowLayout.implicitWidth + Appearance.padding.small * 2 + implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 radius: height / 2 StateLayer { @@ -27,7 +27,8 @@ CustomRect { id: rowLayout anchors.centerIn: parent - spacing: 0 + implicitHeight: root.implicitHeight + spacing: Appearance.spacing.smaller Ref { service: SystemUsage @@ -35,6 +36,7 @@ CustomRect { Resource { Layout.alignment: Qt.AlignVCenter + Layout.fillHeight: true icon: "memory" mainColor: DynamicColors.palette.m3primary percentage: SystemUsage.cpuPerc @@ -42,6 +44,7 @@ CustomRect { } Resource { + Layout.fillHeight: true icon: "memory_alt" mainColor: DynamicColors.palette.m3secondary percentage: SystemUsage.memPerc @@ -49,12 +52,14 @@ CustomRect { } Resource { + Layout.fillHeight: true icon: "gamepad" mainColor: DynamicColors.palette.m3tertiary percentage: SystemUsage.gpuPerc } Resource { + Layout.fillHeight: true icon: "developer_board" mainColor: DynamicColors.palette.m3primary percentage: SystemUsage.gpuMemUsed -- 2.47.3 From 401ccef90c541b812c71c0cb2d8ecba5253805c6 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Wed, 11 Mar 2026 19:49:19 +0100 Subject: [PATCH 15/47] slight changes --- Modules/AudioWidget.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/AudioWidget.qml b/Modules/AudioWidget.qml index 64c4f46..2a99d5f 100644 --- a/Modules/AudioWidget.qml +++ b/Modules/AudioWidget.qml @@ -45,7 +45,7 @@ CustomRect { CustomRect { Layout.fillWidth: true - color: "#50ffffff" + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) implicitHeight: 4 radius: 20 @@ -74,7 +74,7 @@ CustomRect { CustomRect { Layout.fillWidth: true - color: "#50ffffff" + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) implicitHeight: 4 radius: 20 -- 2.47.3 From 851b78f0ffa481ffe52fb16397e69f6cf8ecb7e0 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Thu, 12 Mar 2026 10:04:27 +0100 Subject: [PATCH 16/47] kek test --- Helpers/DesktopUtils.qml | 201 +++++++++++++ Helpers/FileUtils.qml | 28 ++ .../DesktopIcons/BackgroundContextMenu.qml | 150 ++++++++++ .../DesktopIcons/DesktopIconContextMenu.qml | 193 +++++++++++++ Modules/DesktopIcons/DesktopIconDelegate.qml | 273 ++++++++++++++++++ Modules/DesktopIcons/DesktopIcons.qml | 162 +++++++++++ Modules/MediaWidget.qml | 2 +- Modules/Resource.qml | 80 ++--- Modules/UpdatesWidget.qml | 2 +- Modules/Wallpaper/Wallpaper.qml | 4 + Paths/Paths.qml | 1 + Plugins/ZShell/CMakeLists.txt | 1 + Plugins/ZShell/Services/CMakeLists.txt | 4 + Plugins/ZShell/Services/desktopmodel.cpp | 186 ++++++++++++ Plugins/ZShell/Services/desktopmodel.hpp | 46 +++ .../ZShell/Services/desktopstatemanager.cpp | 54 ++++ .../ZShell/Services/desktopstatemanager.hpp | 24 ++ 17 files changed, 1347 insertions(+), 64 deletions(-) create mode 100644 Helpers/DesktopUtils.qml create mode 100644 Helpers/FileUtils.qml create mode 100644 Modules/DesktopIcons/BackgroundContextMenu.qml create mode 100644 Modules/DesktopIcons/DesktopIconContextMenu.qml create mode 100644 Modules/DesktopIcons/DesktopIconDelegate.qml create mode 100644 Modules/DesktopIcons/DesktopIcons.qml create mode 100644 Plugins/ZShell/Services/desktopmodel.cpp create mode 100644 Plugins/ZShell/Services/desktopmodel.hpp create mode 100644 Plugins/ZShell/Services/desktopstatemanager.cpp create mode 100644 Plugins/ZShell/Services/desktopstatemanager.hpp diff --git a/Helpers/DesktopUtils.qml b/Helpers/DesktopUtils.qml new file mode 100644 index 0000000..657eb73 --- /dev/null +++ b/Helpers/DesktopUtils.qml @@ -0,0 +1,201 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + function getAppId(fileName) { + return fileName.endsWith(".desktop") ? fileName.replace(".desktop", "") : null; + } + + function getFileType(fileName, isDir) { + if (isDir) + return "directory"; + let ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : ""; + if (ext === "desktop") + return "desktop"; + + const map = { + "image": ["png", "jpg", "jpeg", "svg", "gif", "bmp", "webp", "ico", "tiff", "tif", "heic", "heif", "raw", "psd", "ai", "xcf"], + "video": ["mp4", "mkv", "webm", "avi", "mov", "flv", "wmv", "m4v", "mpg", "mpeg", "3gp", "vob", "ogv", "ts"], + "audio": ["mp3", "wav", "flac", "aac", "ogg", "m4a", "wma", "opus", "alac", "mid", "midi", "amr"], + "archive": ["zip", "tar", "gz", "rar", "7z", "xz", "bz2", "tgz", "iso", "img", "dmg", "deb", "rpm", "apk"], + "document": ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", "rtf", "epub", "mobi", "djvu"], + "text": ["txt", "md", "rst", "tex", "log", "json", "xml", "yaml", "yml", "toml", "ini", "conf", "cfg", "env", "csv", "tsv"], + "code": ["qml", "cpp", "c", "h", "hpp", "py", "js", "ts", "jsx", "tsx", "java", "rs", "go", "rb", "php", "cs", "swift", "kt", "sh", "bash", "zsh", "fish", "html", "htm", "css", "scss", "sass", "less", "vue", "svelte", "sql", "graphql", "lua", "pl", "dart", "r", "dockerfile", "make"], + "executable": ["exe", "msi", "bat", "cmd", "appimage", "run", "bin", "out", "so", "dll"], + "font": ["ttf", "otf", "woff", "woff2"] + }; + + for (const [type, extensions] of Object.entries(map)) { + if (extensions.includes(ext)) + return type; + } + return "unknown"; + } + + function getIconName(fileName, isDir) { + if (isDir) + return "folder"; + let ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : ""; + + const map = { + // Images + "png": "image-x-generic", + "jpg": "image-x-generic", + "jpeg": "image-x-generic", + "svg": "image-svg+xml", + "gif": "image-x-generic", + "bmp": "image-x-generic", + "webp": "image-x-generic", + "ico": "image-x-generic", + "tiff": "image-x-generic", + "tif": "image-x-generic", + "heic": "image-x-generic", + "heif": "image-x-generic", + "raw": "image-x-generic", + "psd": "image-vnd.adobe.photoshop", + "ai": "application-illustrator", + "xcf": "image-x-xcf", + + // Vidéos + "mp4": "video-x-generic", + "mkv": "video-x-generic", + "webm": "video-x-generic", + "avi": "video-x-generic", + "mov": "video-x-generic", + "flv": "video-x-generic", + "wmv": "video-x-generic", + "m4v": "video-x-generic", + "mpg": "video-x-generic", + "mpeg": "video-x-generic", + "3gp": "video-x-generic", + "vob": "video-x-generic", + "ogv": "video-x-generic", + "ts": "video-x-generic", + + // Audio + "mp3": "audio-x-generic", + "wav": "audio-x-generic", + "flac": "audio-x-generic", + "aac": "audio-x-generic", + "ogg": "audio-x-generic", + "m4a": "audio-x-generic", + "wma": "audio-x-generic", + "opus": "audio-x-generic", + "alac": "audio-x-generic", + "mid": "audio-midi", + "midi": "audio-midi", + "amr": "audio-x-generic", + + // Archives & Images + "zip": "application-zip", + "tar": "application-x-tar", + "gz": "application-gzip", + "rar": "application-vnd.rar", + "7z": "application-x-7z-compressed", + "xz": "application-x-xz", + "bz2": "application-x-bzip2", + "tgz": "application-x-compressed-tar", + "iso": "application-x-cd-image", + "img": "application-x-cd-image", + "dmg": "application-x-apple-diskimage", + "deb": "application-vnd.debian.binary-package", + "rpm": "application-x-rpm", + "apk": "application-vnd.android.package-archive", + + // Documents + "pdf": "application-pdf", + "doc": "application-msword", + "docx": "application-vnd.openxmlformats-officedocument.wordprocessingml.document", + "xls": "application-vnd.ms-excel", + "xlsx": "application-vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "ppt": "application-vnd.ms-powerpoint", + "pptx": "application-vnd.openxmlformats-officedocument.presentationml.presentation", + "odt": "application-vnd.oasis.opendocument.text", + "ods": "application-vnd.oasis.opendocument.spreadsheet", + "odp": "application-vnd.oasis.opendocument.presentation", + "rtf": "application-rtf", + "epub": "application-epub+zip", + "mobi": "application-x-mobipocket-ebook", + "djvu": "image-vnd.djvu", + "csv": "text-csv", + "tsv": "text-tab-separated-values", + + // Data & Config + "txt": "text-x-generic", + "md": "text-markdown", + "rst": "text-x-rst", + "tex": "text-x-tex", + "log": "text-x-log", + "json": "application-json", + "xml": "text-xml", + "yaml": "text-x-yaml", + "yml": "text-x-yaml", + "toml": "text-x-toml", + "ini": "text-x-generic", + "conf": "text-x-generic", + "cfg": "text-x-generic", + "env": "text-x-generic", + + // Code + "qml": "text-x-qml", + "cpp": "text-x-c++src", + "c": "text-x-csrc", + "h": "text-x-chdr", + "hpp": "text-x-c++hdr", + "py": "text-x-python", + "js": "text-x-javascript", + "ts": "text-x-typescript", + "jsx": "text-x-javascript", + "tsx": "text-x-typescript", + "java": "text-x-java", + "rs": "text-x-rust", + "go": "text-x-go", + "rb": "text-x-ruby", + "php": "application-x-php", + "cs": "text-x-csharp", + "swift": "text-x-swift", + "kt": "text-x-kotlin", + "sh": "application-x-shellscript", + "bash": "application-x-shellscript", + "zsh": "application-x-shellscript", + "fish": "application-x-shellscript", + "html": "text-html", + "htm": "text-html", + "css": "text-css", + "scss": "text-x-scss", + "sass": "text-x-sass", + "less": "text-x-less", + "vue": "text-html", + "svelte": "text-html", + "sql": "application-x-sql", + "graphql": "text-x-generic", + "lua": "text-x-lua", + "pl": "text-x-perl", + "dart": "text-x-dart", + "r": "text-x-r", + "dockerfile": "text-x-generic", + "make": "text-x-makefile", + + // Executables + "exe": "application-x-executable", + "msi": "application-x-msi", + "bat": "application-x-ms-dos-executable", + "cmd": "application-x-ms-dos-executable", + "appimage": "application-x-executable", + "run": "application-x-executable", + "bin": "application-x-executable", + "out": "application-x-executable", + "so": "application-x-sharedlib", + "dll": "application-x-sharedlib", + + // Fonts + "ttf": "font-x-generic", + "otf": "font-x-generic", + "woff": "font-x-generic", + "woff2": "font-x-generic" + }; + return map[ext] || "text-x-generic"; + } +} diff --git a/Helpers/FileUtils.qml b/Helpers/FileUtils.qml new file mode 100644 index 0000000..9690a6c --- /dev/null +++ b/Helpers/FileUtils.qml @@ -0,0 +1,28 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + function fileNameForPath(str) { + if (typeof str !== "string") + return ""; + const trimmed = trimFileProtocol(str); + return trimmed.split(/[\\/]/).pop(); + } + + function trimFileExt(str) { + if (typeof str !== "string") + return ""; + const trimmed = trimFileProtocol(str); + const lastDot = trimmed.lastIndexOf("."); + if (lastDot > -1 && lastDot > trimmed.lastIndexOf("/")) { + return trimmed.slice(0, lastDot); + } + return trimmed; + } + + function trimFileProtocol(str) { + return str.startsWith("file://") ? str.slice(7) : str; + } +} diff --git a/Modules/DesktopIcons/BackgroundContextMenu.qml b/Modules/DesktopIcons/BackgroundContextMenu.qml new file mode 100644 index 0000000..0b96270 --- /dev/null +++ b/Modules/DesktopIcons/BackgroundContextMenu.qml @@ -0,0 +1,150 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Hyprland +import qs.Components +import qs.Config +import qs.Paths + +Item { + id: root + + anchors.fill: parent + z: 998 + visible: false + + property real menuX: 0 + property real menuY: 0 + + MouseArea { + anchors.fill: parent + onClicked: root.close() + } + + CustomClippingRect { + id: popupBackground + readonly property real padding: 4 + + x: root.menuX + y: root.menuY + + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.normal + + implicitWidth: menuLayout.implicitWidth + padding * 2 + implicitHeight: menuLayout.implicitHeight + padding * 2 + + Behavior on opacity { Anim {} } + opacity: root.visible ? 1 : 0 + + ColumnLayout { + id: menuLayout + anchors.fill: parent + anchors.margins: popupBackground.padding + spacing: 0 + + StateLayer { + Layout.fillWidth: true + + contentItem: RowLayout { + spacing: 8 + anchors.fill: parent + anchors.margins: 12 + MaterialIcon { text: "terminal"; font.pointSize: 20 } + CustomText { text: "Open terminal"; Layout.fillWidth: true } + } + + onClicked: { + Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)]) + root.close() + } + } + + CustomRect { + Layout.fillWidth: true + implicitHeight: 1 + color: DynamicColors.palette.m3outlineVariant + Layout.topMargin: 4 + Layout.bottomMargin: 4 + } + + StateLayer { + Layout.fillWidth: true + + contentItem: RowLayout { + spacing: 8 + anchors.fill: parent + anchors.margins: 12 + MaterialIcon { text: "settings"; font.pointSize: 20 } + CustomText { text: "Sleex settings"; Layout.fillWidth: true } + } + + onClicked: { + Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"]) + root.close() + } + } + + CustomRect { + Layout.fillWidth: true + implicitHeight: 1 + color: Appearance.m3colors.m3outlineVariant + Layout.topMargin: 4 + Layout.bottomMargin: 4 + } + + StateLayer { + Layout.fillWidth: true + + contentItem: RowLayout { + spacing: 8 + anchors.fill: parent + anchors.margins: 12 + MaterialIcon { text: "logout"; font.pointSize: 20 } + CustomText { text: "Logout"; Layout.fillWidth: true } + } + + onClicked: { + Hyprland.dispatch("global quickshell:sessionOpen") + root.close() + } + } + + CustomRect { + Layout.fillWidth: true + implicitHeight: 1 + color: Appearance.m3colors.m3outlineVariant + Layout.topMargin: 4 + Layout.bottomMargin: 4 + } + + StateLayer { + Layout.fillWidth: true + + contentItem: RowLayout { + spacing: 8 + anchors.fill: parent + anchors.margins: 12 + MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 } + CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true } + } + + onClicked: { + Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons + root.close() + } + } + } + } + + function openAt(mouseX, mouseY, parentW, parentH) { + menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth) + menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight) + visible = true + } + + function close() { + visible = false + } +} diff --git a/Modules/DesktopIcons/DesktopIconContextMenu.qml b/Modules/DesktopIcons/DesktopIconContextMenu.qml new file mode 100644 index 0000000..238246f --- /dev/null +++ b/Modules/DesktopIcons/DesktopIconContextMenu.qml @@ -0,0 +1,193 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs.Components +import qs.Config + +Item { + id: contextMenu + + anchors.fill: parent + z: 999 + visible: false + + property string targetFilePath: "" + property bool targetIsDir: false + property var targetAppEntry: null + + property var targetPaths: [] + + signal openFileRequested(string path, bool isDir) + signal renameRequested(string path) + + property real menuX: 0 + property real menuY: 0 + + MouseArea { + anchors.fill: parent + onClicked: contextMenu.close() + } + + CustomClippingRect { + id: popupBackground + readonly property real padding: 4 + + x: contextMenu.menuX + y: contextMenu.menuY + + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.normal + + implicitWidth: menuLayout.implicitWidth + padding * 2 + implicitHeight: menuLayout.implicitHeight + padding * 2 + + Behavior on opacity { Anim {} } + opacity: contextMenu.visible ? 1 : 0 + + ColumnLayout { + id: menuLayout + anchors.fill: parent + anchors.margins: popupBackground.padding + spacing: 0 + + StateLayer { + Layout.fillWidth: true + + contentItem: RowLayout { + spacing: 8 + anchors.fill: parent + anchors.margins: 12 + MaterialIcon { text: "open_in_new"; font.pointSize: 20 } + CustomText { text: "Open"; Layout.fillWidth: true } + } + + onClicked: { + for (let i = 0; i < contextMenu.targetPaths.length; i++) { + let p = contextMenu.targetPaths[i]; + if (p === contextMenu.targetFilePath) { + if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute() + else contextMenu.openFileRequested(p, contextMenu.targetIsDir) + } else { + Quickshell.execDetached(["xdg-open", p]) + } + } + contextMenu.close() + } + } + + StateLayer { + Layout.fillWidth: true + + contentItem: RowLayout { + spacing: 8 + anchors.fill: parent + anchors.margins: 12 + MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 } + CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true } + } + + onClicked: { + Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]) + contextMenu.close() + } + } + + CustomRect { + Layout.fillWidth: true + implicitHeight: 1 + color: DynamicColors.palette.m3outlineVariant + Layout.topMargin: 4 + Layout.bottomMargin: 4 + } + + StateLayer { + Layout.fillWidth: true + + contentItem: RowLayout { + spacing: 8 + anchors.fill: parent + anchors.margins: 12 + MaterialIcon { text: "content_copy"; font.pointSize: 20 } + CustomText { text: "Copy path"; Layout.fillWidth: true } + } + + onClicked: { + Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]) + contextMenu.close() + } + } + + StateLayer { + Layout.fillWidth: true + visible: contextMenu.targetPaths.length === 1 + + contentItem: RowLayout { + spacing: 8 + anchors.fill: parent + anchors.margins: 12 + MaterialIcon { text: "edit"; font.pointSize: 20 } + CustomText { text: "Rename"; Layout.fillWidth: true } + } + + onClicked: { + contextMenu.renameRequested(contextMenu.targetFilePath) + contextMenu.close() + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 1 + color: Appearance.m3colors.m3outlineVariant + Layout.topMargin: 4 + Layout.bottomMargin: 4 + } + + StateLayer { + id: deleteButton + Layout.fillWidth: true + colBackgroundHover: Appearance.colors.colError + + contentItem: RowLayout { + spacing: 8 + anchors.fill: parent + anchors.margins: 12 + MaterialIcon { + text: "delete"; + font.pointSize: 20; + color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError + } + CustomText { + text: "Move to trash"; + Layout.fillWidth: true; + color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError + } + } + + onClicked: { + let cmd = ["gio", "trash"].concat(contextMenu.targetPaths) + Quickshell.execDetached(cmd) + contextMenu.close() + } + } + } + } + + function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) { + targetFilePath = path + targetIsDir = isDir + targetAppEntry = appEnt + + targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path] + + menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth) + menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight) + + visible = true + } + + function close() { + visible = false + } +} diff --git a/Modules/DesktopIcons/DesktopIconDelegate.qml b/Modules/DesktopIcons/DesktopIconDelegate.qml new file mode 100644 index 0000000..2ec681c --- /dev/null +++ b/Modules/DesktopIcons/DesktopIconDelegate.qml @@ -0,0 +1,273 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Config +import qs.Components +import qs.Helpers + +Item { + id: delegateRoot + + property var appEntry: fileName.endsWith(".desktop") ? DesktopEntries.byId(DesktopUtils.getAppId(fileName)) : null + property bool fileIsDir: model.isDir + property string fileName: model.fileName + property string filePath: model.filePath + property int gridX: model.gridX + property int gridY: model.gridY + property bool isSnapping: snapAnimX.running || snapAnimY.running + property string resolvedIcon: { + if (fileName.endsWith(".desktop")) { + if (appEntry && appEntry.icon && appEntry.icon !== "") + return appEntry.icon; + return AppSearch.guessIcon(DesktopUtils.getAppId(fileName)); + } else if (DesktopUtils.getFileType(fileName, fileIsDir) === "image") { + return "file://" + filePath; + } else { + return DesktopUtils.getIconName(fileName, fileIsDir); + } + } + + function compensateAndSnap(absVisX, absVisY) { + dragContainer.x = absVisX - delegateRoot.x; + dragContainer.y = absVisY - delegateRoot.y; + snapAnimX.start(); + snapAnimY.start(); + } + + function getDragX() { + return dragContainer.x; + } + + function getDragY() { + return dragContainer.y; + } + + height: root.cellHeight + width: root.cellWidth + x: gridX * root.cellWidth + y: gridY * root.cellHeight + + Behavior on x { + enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath) + + Anim { + } + } + Behavior on y { + enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath) + + Anim { + } + } + + Item { + id: dragContainer + + height: parent.height + width: parent.width + + states: State { + when: mouseArea.drag.active + + PropertyChanges { + opacity: 0.8 + scale: 1.1 + target: dragContainer + z: 100 + } + } + transform: Translate { + x: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragX : 0 + y: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragY : 0 + } + transitions: Transition { + Anim { + } + } + + onXChanged: { + if (mouseArea.drag.active) { + root.dragLeader = filePath; + root.groupDragX = x; + } + } + onYChanged: { + if (mouseArea.drag.active) { + root.dragLeader = filePath; + root.groupDragY = y; + } + } + + PropertyAnimation { + id: snapAnimX + + duration: 250 + easing.type: Easing.OutCubic + property: "x" + target: dragContainer + to: 0 + } + + PropertyAnimation { + id: snapAnimY + + duration: 250 + easing.type: Easing.OutCubic + property: "y" + target: dragContainer + to: 0 + } + + Column { + anchors.centerIn: parent + spacing: 6 + + IconImage { + anchors.horizontalCenter: parent.horizontalCenter + implicitSize: 48 + source: { + if (delegateRoot.resolvedIcon.startsWith("file://") || delegateRoot.resolvedIcon.startsWith("/")) { + return delegateRoot.resolvedIcon; + } else { + return Quickshell.iconPath(delegateRoot.resolvedIcon, fileIsDir ? "folder" : "text-x-generic"); + } + } + } + + Item { + height: 40 + width: 88 + + CustomText { + anchors.fill: parent + color: "white" + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 2 + style: Text.Outline + styleColor: "black" + text: (appEntry && appEntry.name !== "") ? appEntry.name : fileName + visible: !renameLoader.active + wrapMode: Text.Wrap + } + + Loader { + id: renameLoader + + active: root.editingFilePath === filePath + anchors.centerIn: parent + height: 24 + width: 110 + + sourceComponent: CustomTextInput { + anchors.fill: parent + anchors.margins: 2 + color: "white" + horizontalAlignment: Text.AlignHCenter + text: fileName + wrapMode: Text.Wrap + + Component.onCompleted: { + forceActiveFocus(); + selectAll(); + } + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (text.trim() !== "" && text !== fileName) { + let newName = text.trim(); + let newPath = filePath.substring(0, filePath.lastIndexOf('/') + 1) + newName; + + Quickshell.execDetached(["mv", filePath, newPath]); + } + root.editingFilePath = ""; + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + root.editingFilePath = ""; + event.accepted = true; + } + } + onActiveFocusChanged: { + if (!activeFocus && root.editingFilePath === filePath) { + root.editingFilePath = ""; + } + } + } + } + } + } + + CustomRect { + anchors.fill: parent + anchors.margins: 4 + color: "white" + opacity: root.selectedIcons.includes(filePath) ? 0.2 : 0.0 + radius: 8 + + Behavior on opacity { + Anim { + } + } + } + + MouseArea { + id: mouseArea + + acceptedButtons: Qt.LeftButton | Qt.RightButton + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + drag.target: dragContainer + hoverEnabled: true + + onClicked: mouse => { + root.forceActiveFocus(); + + if (mouse.button === Qt.RightButton) { + if (!root.selectedIcons.includes(filePath)) { + root.selectedIcons = [filePath]; + } + let pos = mapToItem(root, mouse.x, mouse.y); + root.contextMenu.openAt(pos.x, pos.y, filePath, fileIsDir, appEntry, root.width, root.height, root.selectedIcons); + } else { + root.selectedIcons = [filePath]; + root.contextMenu.close(); + } + } + onDoubleClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + if (filePath.endsWith(".desktop") && appEntry) + appEntry.execute(); + else + root.exec(filePath, fileIsDir); + } + } + onPressed: mouse => { + if (mouse.button === Qt.LeftButton && !root.selectedIcons.includes(filePath)) { + root.selectedIcons = [filePath]; + } + } + onReleased: { + if (drag.active) { + let absoluteX = delegateRoot.x + dragContainer.x; + let absoluteY = delegateRoot.y + dragContainer.y; + let snapX = Math.max(0, Math.round(absoluteX / root.cellWidth)); + let snapY = Math.max(0, Math.round(absoluteY / root.cellHeight)); + + root.performMassDrop(filePath, snapX, snapY); + } + } + + CustomRect { + anchors.fill: parent + anchors.margins: 4 + color: "white" + opacity: parent.containsMouse ? 0.1 : 0.0 + radius: 8 + + Behavior on opacity { + Anim { + } + } + } + } + } +} diff --git a/Modules/DesktopIcons/DesktopIcons.qml b/Modules/DesktopIcons/DesktopIcons.qml new file mode 100644 index 0000000..03774b9 --- /dev/null +++ b/Modules/DesktopIcons/DesktopIcons.qml @@ -0,0 +1,162 @@ +import QtQuick +import Quickshell +import qs.Modules +import qs.Helpers +import qs.Paths +import ZShell.Services + +Item { + id: root + + property int cellHeight: 110 + property int cellWidth: 100 + property var contextMenu: desktopMenu + property string dragLeader: "" + property string editingFilePath: "" + property real groupDragX: 0 + property real groupDragY: 0 + property var selectedIcons: [] + property real startX: 0 + property real startY: 0 + + function exec(filePath, isDir) { + const cmd = ["xdg-open", filePath]; + Quickshell.execDetached(cmd); + } + + function performMassDrop(leaderPath, targetX, targetY) { + let maxCol = Math.max(0, Math.floor(gridArea.width / cellWidth) - 1); + let maxRow = Math.max(0, Math.floor(gridArea.height / cellHeight) - 1); + + let visuals = []; + for (let i = 0; i < gridArea.children.length; i++) { + let child = gridArea.children[i]; + if (child.filePath && root.selectedIcons.includes(child.filePath)) { + let isLeader = (root.dragLeader === child.filePath); + let offsetX = isLeader ? child.getDragX() : root.groupDragX; + let offsetY = isLeader ? child.getDragY() : root.groupDragY; + visuals.push({ + childRef: child, + absX: child.x + offsetX, + absY: child.y + offsetY + }); + } + } + + desktopModel.massMove(root.selectedIcons, leaderPath, targetX, targetY, maxCol, maxRow); + + for (let i = 0; i < visuals.length; i++) { + visuals[i].childRef.compensateAndSnap(visuals[i].absX, visuals[i].absY); + } + + root.dragLeader = ""; + root.groupDragX = 0; + root.groupDragY = 0; + } + + anchors.fill: parent + focus: true + + Keys.onPressed: event => { + if (event.key === Qt.Key_F2 && selectedIcons.length > 0) + editingFilePath = selectedIcons[0]; + } + + DesktopModel { + id: desktopModel + + Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop)) + } + + Rectangle { + id: lasso + + border.color: Appearance.colors.colPrimary + border.width: 1 + color: DynamicColors.tPalette.m3primary + radius: Appearance.rounding.small + visible: false + z: 99 + } + + MouseArea { + acceptedButtons: Qt.LeftButton | Qt.RightButton + anchors.fill: parent + + onPositionChanged: mouse => { + if (lasso.visible) { + lasso.x = Math.min(mouse.x, root.startX); + lasso.y = Math.min(mouse.y, root.startY); + lasso.width = Math.abs(mouse.x - root.startX); + lasso.height = Math.abs(mouse.y - root.startY); + + let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth); + let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth); + let minRow = Math.floor((lasso.y - gridArea.y) / cellHeight); + let maxRow = Math.floor((lasso.y + lasso.height - gridArea.y) / cellHeight); + + let newSelection = []; + for (let i = 0; i < gridArea.children.length; i++) { + let child = gridArea.children[i]; + if (child.filePath !== undefined && child.gridX >= minCol && child.gridX <= maxCol && child.gridY >= minRow && child.gridY <= maxRow) { + newSelection.push(child.filePath); + } + } + root.selectedIcons = newSelection; + } + } + onPressed: mouse => { + root.editingFilePath = ""; + desktopMenu.close(); + + if (mouse.button === Qt.RightButton) { + root.selectedIcons = []; + bgContextMenu.openAt(mouse.x, mouse.y, root.width, root.height); + } else { + bgContextMenu.close(); + root.selectedIcons = []; + root.startX = mouse.x; + root.startY = mouse.y; + lasso.x = mouse.x; + lasso.y = mouse.y; + lasso.width = 0; + lasso.height = 0; + lasso.visible = true; + } + } + onReleased: { + lasso.visible = false; + } + } + + Item { + id: gridArea + + anchors.fill: parent + anchors.margins: 20 + anchors.topMargin: 40 + visible: true + + Repeater { + model: desktopModel + + delegate: DesktopIconDelegate { + property int itemIndex: index + } + } + } + + DesktopIconContextMenu { + id: desktopMenu + + onOpenFileRequested: (path, isDir) => root.exec(path, isDir) + onRenameRequested: path => { + root.editingFilePath = path; + } + } + + BackgroundContextMenu { + id: bgContextMenu + + } +} diff --git a/Modules/MediaWidget.qml b/Modules/MediaWidget.qml index 6979b43..1b7c821 100644 --- a/Modules/MediaWidget.qml +++ b/Modules/MediaWidget.qml @@ -41,7 +41,7 @@ CustomRect { MaterialIcon { animate: true color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface - font.pointSize: Appearance.font.size.normal + font.pointSize: Appearance.font.size.larger text: Players.active?.isPlaying ? "music_note" : "music_off" } diff --git a/Modules/Resource.qml b/Modules/Resource.qml index 89b16a4..43c4561 100644 --- a/Modules/Resource.qml +++ b/Modules/Resource.qml @@ -19,91 +19,47 @@ RowLayout { property bool warning: percentage * 100 >= warningThreshold property int warningThreshold: 80 - clip: true percentage: 0 Behavior on animatedPercentage { Anim { - duration: Appearance.anim.durations.large } } Component.onCompleted: animatedPercentage = percentage - onPercentageChanged: animatedPercentage = percentage + onPercentageChanged: { + const next = percentage; - // Canvas { - // id: gaugeCanvas - // - // anchors.centerIn: parent - // height: width - // width: Math.min(parent.width, parent.height) - // - // Component.onCompleted: requestPaint() - // onPaint: { - // const ctx = getContext("2d"); - // ctx.reset(); - // const cx = width / 2; - // const cy = (height / 2) + 1; - // const radius = (Math.min(width, height) - 12) / 2; - // const lineWidth = 3; - // ctx.beginPath(); - // ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); - // ctx.lineWidth = lineWidth; - // ctx.lineCap = "round"; - // ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); - // ctx.stroke(); - // if (root.animatedPercentage > 0) { - // ctx.beginPath(); - // ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage); - // ctx.lineWidth = lineWidth; - // ctx.lineCap = "round"; - // ctx.strokeStyle = root.accentColor; - // ctx.stroke(); - // } - // } - // - // Connections { - // function onAnimatedPercentageChanged() { - // gaugeCanvas.requestPaint(); - // } - // - // target: root - // } - // - // Connections { - // function onPaletteChanged() { - // gaugeCanvas.requestPaint(); - // } - // - // target: DynamicColors - // } - // } + if (Math.abs(next - animatedPercentage) >= 0.05) + animatedPercentage = next; + } MaterialIcon { id: icon color: DynamicColors.palette.m3onSurface - font.pointSize: 12 + font.pointSize: Appearance.font.size.larger text: root.icon } CustomClippingRect { - Layout.preferredHeight: root.height - Layout.preferredWidth: 5 + Layout.preferredHeight: root.height - Appearance.padding.small + Layout.preferredWidth: 4 color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) radius: Appearance.rounding.full CustomRect { - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - color: root.mainColor - implicitHeight: root.percentage * parent.height - radius: implicitHeight / 2 + id: fill - Behavior on implicitHeight { - Anim { - } + anchors.fill: parent + antialiasing: false + color: root.mainColor + implicitHeight: Math.ceil(root.percentage * parent.height) + radius: Appearance.rounding.full + + transform: Scale { + origin.y: fill.height + yScale: Math.max(0.001, root.animatedPercentage) } } } diff --git a/Modules/UpdatesWidget.qml b/Modules/UpdatesWidget.qml index e6166b7..36cdc67 100644 --- a/Modules/UpdatesWidget.qml +++ b/Modules/UpdatesWidget.qml @@ -22,7 +22,7 @@ CustomRect { spacing: Appearance.spacing.small MaterialIcon { - font.pointSize: Appearance.font.size.normal + font.pointSize: Appearance.font.size.larger text: "package_2" } diff --git a/Modules/Wallpaper/Wallpaper.qml b/Modules/Wallpaper/Wallpaper.qml index fa77580..06f940d 100644 --- a/Modules/Wallpaper/Wallpaper.qml +++ b/Modules/Wallpaper/Wallpaper.qml @@ -2,6 +2,7 @@ import Quickshell import QtQuick import Quickshell.Wayland import qs.Config +import qs.Modules.DesktopIcons Loader { active: Config.background.enabled @@ -30,6 +31,9 @@ Loader { WallBackground { } + + DesktopIcons { + } } } } diff --git a/Paths/Paths.qml b/Paths/Paths.qml index fe33f82..eacf7a7 100644 --- a/Paths/Paths.qml +++ b/Paths/Paths.qml @@ -10,6 +10,7 @@ Singleton { readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell` readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell` readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell` + readonly property string desktop: `${Quickshell.env("XDG_DATA_HOME") || `${home}/Desktop`}` readonly property string home: Quickshell.env("HOME") readonly property string imagecache: `${cache}/imagecache` readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell" diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index 9b0b751..fda2330 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/CMakeLists.txt @@ -4,6 +4,7 @@ pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) +pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0) if(NOT Cava_FOUND) pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) endif() diff --git a/Plugins/ZShell/Services/CMakeLists.txt b/Plugins/ZShell/Services/CMakeLists.txt index 39ecda2..bc4274f 100644 --- a/Plugins/ZShell/Services/CMakeLists.txt +++ b/Plugins/ZShell/Services/CMakeLists.txt @@ -7,7 +7,11 @@ qml_module(ZShell-services audiocollector.hpp audiocollector.cpp audioprovider.hpp audioprovider.cpp cavaprovider.hpp cavaprovider.cpp + desktopmodel.hpp desktopmodel.cpp + desktopstatemanager.hpp desktopstatemanager.cpp LIBRARIES + Qt6::Core + Qt6::Qml PkgConfig::Pipewire PkgConfig::Aubio PkgConfig::Cava diff --git a/Plugins/ZShell/Services/desktopmodel.cpp b/Plugins/ZShell/Services/desktopmodel.cpp new file mode 100644 index 0000000..a75cd98 --- /dev/null +++ b/Plugins/ZShell/Services/desktopmodel.cpp @@ -0,0 +1,186 @@ +#include "desktopmodel.hpp" +#include "desktopstatemanager.hpp" +#include +#include + +namespace ZShell::services { + +DesktopModel::DesktopModel(QObject *parent) : QAbstractListModel(parent) { +} + +int DesktopModel::rowCount(const QModelIndex &parent) const { + if (parent.isValid()) return 0; + return m_items.count(); +} + +QVariant DesktopModel::data(const QModelIndex &index, int role) const { + if (!index.isValid() || index.row() >= m_items.size()) return QVariant(); + + const DesktopItem &item = m_items[index.row()]; + switch (role) { + case FileNameRole: return item.fileName; + case FilePathRole: return item.filePath; + case IsDirRole: return item.isDir; + case GridXRole: return item.gridX; + case GridYRole: return item.gridY; + default: return QVariant(); + } +} + +QHash DesktopModel::roleNames() const { + QHash roles; + roles[FileNameRole] = "fileName"; + roles[FilePathRole] = "filePath"; + roles[IsDirRole] = "isDir"; + roles[GridXRole] = "gridX"; + roles[GridYRole] = "gridY"; + return roles; +} + +void DesktopModel::loadDirectory(const QString &path) { + beginResetModel(); + m_items.clear(); + + QDir dir(path); + dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + QFileInfoList list = dir.entryInfoList(); + + DesktopStateManager sm; + QVariantMap savedLayout = sm.getLayout(); + + for (const QFileInfo &fileInfo : list) { + DesktopItem item; + item.fileName = fileInfo.fileName(); + item.filePath = fileInfo.absoluteFilePath(); + item.isDir = fileInfo.isDir(); + + if (savedLayout.contains(item.fileName)) { + QVariantMap pos = savedLayout[item.fileName].toMap(); + item.gridX = pos["x"].toInt(); + item.gridY = pos["y"].toInt(); + } else { + // TODO: make getEmptySpot in C++ and call it here to get the initial position for new icons + item.gridX = 0; + item.gridY = 0; + } + m_items.append(item); + } + endResetModel(); +} + +void DesktopModel::moveIcon(int index, int newX, int newY) { + if (index < 0 || index >= m_items.size()) return; + + m_items[index].gridX = newX; + m_items[index].gridY = newY; + + QModelIndex modelIndex = createIndex(index, 0); + emit dataChanged(modelIndex, modelIndex, {GridXRole, GridYRole}); + + saveCurrentLayout(); +} + +void DesktopModel::saveCurrentLayout() { + QVariantMap layout; + for (const auto& item : m_items) { + QVariantMap pos; + pos["x"] = item.gridX; + pos["y"] = item.gridY; + layout[item.fileName] = pos; + } + + DesktopStateManager sm; + sm.saveLayout(layout); +} + +void DesktopModel::massMove(const QVariantList& selectedPathsList, const QString& leaderPath, int targetX, int targetY, int maxCol, int maxRow) { + QStringList selectedPaths; + for (const QVariant& v : selectedPathsList) { + selectedPaths << v.toString(); + } + + if (selectedPaths.isEmpty()) return; + + int oldX = 0, oldY = 0; + for (const auto& item : m_items) { + if (item.filePath == leaderPath) { + oldX = item.gridX; + oldY = item.gridY; + break; + } + } + + int deltaX = targetX - oldX; + int deltaY = targetY - oldY; + + if (deltaX == 0 && deltaY == 0) return; + + if (selectedPaths.size() == 1 && targetX >= 0 && targetX <= maxCol && targetY >= 0 && targetY <= maxRow) { + QString movingPath = selectedPaths.first(); + int movingIndex = -1; + int targetIndex = -1; + + for (int i = 0; i < m_items.size(); ++i) { + if (m_items[i].filePath == movingPath) { + movingIndex = i; + } else if (m_items[i].gridX == targetX && m_items[i].gridY == targetY) { + targetIndex = i; + } + } + + if (targetIndex != -1 && movingIndex != -1) { + m_items[targetIndex].gridX = oldX; + m_items[targetIndex].gridY = oldY; + m_items[movingIndex].gridX = targetX; + m_items[movingIndex].gridY = targetY; + + emit dataChanged(index(0, 0), index(m_items.size() - 1, 0), {GridXRole, GridYRole}); + saveCurrentLayout(); + return; + } + } + + QList movingItems; + QSet occupied; + + for (int i = 0; i < m_items.size(); ++i) { + if (selectedPaths.contains(m_items[i].filePath)) { + movingItems.append(&m_items[i]); + } else { + occupied.insert(QString::number(m_items[i].gridX) + "," + QString::number(m_items[i].gridY)); + } + } + + for (auto* item : movingItems) { + int newX = item->gridX + deltaX; + int newY = item->gridY + deltaY; + + bool outOfBounds = newX < 0 || newX > maxCol || newY < 0 || newY > maxRow; + bool collision = occupied.contains(QString::number(newX) + "," + QString::number(newY)); + + if (outOfBounds || collision) { + bool found = false; + for (int x = 0; x <= maxCol && !found; ++x) { + for (int y = 0; y <= maxRow && !found; ++y) { + QString key = QString::number(x) + "," + QString::number(y); + if (!occupied.contains(key)) { + newX = x; + newY = y; + occupied.insert(key); + found = true; + } + } + } + } else { + occupied.insert(QString::number(newX) + "," + QString::number(newY)); + } + + item->gridX = newX; + item->gridY = newY; + } + + emit dataChanged(index(0, 0), index(m_items.size() - 1, 0), {GridXRole, GridYRole}); + saveCurrentLayout(); +} + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/desktopmodel.hpp b/Plugins/ZShell/Services/desktopmodel.hpp new file mode 100644 index 0000000..d04dcd2 --- /dev/null +++ b/Plugins/ZShell/Services/desktopmodel.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +namespace ZShell::services { + +struct DesktopItem { + QString fileName; + QString filePath; + bool isDir; + int gridX; + int gridY; +}; + +class DesktopModel : public QAbstractListModel { +Q_OBJECT +QML_ELEMENT + +public: +enum DesktopRoles { + FileNameRole = Qt::UserRole + 1, + FilePathRole, + IsDirRole, + GridXRole, + GridYRole +}; + +explicit DesktopModel(QObject *parent = nullptr); + +int rowCount(const QModelIndex &parent = QModelIndex()) const override; +QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; +QHash roleNames() const override; + +Q_INVOKABLE void loadDirectory(const QString &path); +Q_INVOKABLE void moveIcon(int index, int newX, int newY); +Q_INVOKABLE void massMove(const QVariantList &selectedPathsList, const QString &leaderPath, int targetX, int targetY, int maxCol, int maxRow); + +private: +QList m_items; +void saveCurrentLayout(); +}; + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/desktopstatemanager.cpp b/Plugins/ZShell/Services/desktopstatemanager.cpp new file mode 100644 index 0000000..55f9c70 --- /dev/null +++ b/Plugins/ZShell/Services/desktopstatemanager.cpp @@ -0,0 +1,54 @@ +#include "desktopstatemanager.hpp" +#include +#include +#include +#include +#include +#include + +namespace ZShell::services { + +DesktopStateManager::DesktopStateManager(QObject *parent) : QObject(parent) { +} + +QString DesktopStateManager::getConfigFilePath() const { + QString configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/sleex"; + QDir dir(configDir); + if (!dir.exists()) { + dir.mkpath("."); + } + return configDir + "/desktop_layout.json"; +} + +void DesktopStateManager::saveLayout(const QVariantMap& layout) { + QJsonObject jsonObj = QJsonObject::fromVariantMap(layout); + QJsonDocument doc(jsonObj); + QFile file(getConfigFilePath()); + + if (file.open(QIODevice::WriteOnly)) { + file.write(doc.toJson(QJsonDocument::Indented)); + file.close(); + } else { + qWarning() << "Sleex: Impossible de sauvegarder le layout du bureau dans" << getConfigFilePath(); + } +} + +QVariantMap DesktopStateManager::getLayout() { + QFile file(getConfigFilePath()); + + if (!file.open(QIODevice::ReadOnly)) { + return QVariantMap(); + } + + QByteArray data = file.readAll(); + file.close(); + + QJsonDocument doc = QJsonDocument::fromJson(data); + if (doc.isObject()) { + return doc.object().toVariantMap(); + } + + return QVariantMap(); +} + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/desktopstatemanager.hpp b/Plugins/ZShell/Services/desktopstatemanager.hpp new file mode 100644 index 0000000..004b7bb --- /dev/null +++ b/Plugins/ZShell/Services/desktopstatemanager.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace ZShell::services { + +class DesktopStateManager : public QObject { +Q_OBJECT +QML_ELEMENT +QML_SINGLETON + +public: +explicit DesktopStateManager(QObject *parent = nullptr); + +Q_INVOKABLE void saveLayout(const QVariantMap& layout); +Q_INVOKABLE QVariantMap getLayout(); + +private: +QString getConfigFilePath() const; +}; + +} // namespace ZShell::services -- 2.47.3 From 9e9708ed12a90cea06db26c3ad715620e1cc77f8 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Thu, 12 Mar 2026 14:45:20 +0100 Subject: [PATCH 17/47] desktop icons --- Components/CustomTextInput.qml | 15 ++ Config/Config.qml | 1 + Config/General.qml | 1 + Helpers/AppSearch.qml | 122 +++++++++++ .../DesktopIcons/BackgroundContextMenu.qml | 158 ++++++++------ .../DesktopIcons/DesktopIconContextMenu.qml | 193 +++++++++++------- Modules/DesktopIcons/DesktopIconDelegate.qml | 3 +- Modules/DesktopIcons/DesktopIcons.qml | 70 +++++-- Modules/Wallpaper/Wallpaper.qml | 9 +- Paths/Paths.qml | 2 +- scripts/levendist.js | 143 +++++++++++++ 11 files changed, 561 insertions(+), 156 deletions(-) create mode 100644 Components/CustomTextInput.qml create mode 100644 Helpers/AppSearch.qml create mode 100644 scripts/levendist.js diff --git a/Components/CustomTextInput.qml b/Components/CustomTextInput.qml new file mode 100644 index 0000000..25b1eed --- /dev/null +++ b/Components/CustomTextInput.qml @@ -0,0 +1,15 @@ +import QtQuick +import QtQuick.Controls +import qs.Config + +TextInput { + renderType: Text.NativeRendering + selectedTextColor: DynamicColors.palette.m3onSecondaryContainer + selectionColor: DynamicColors.tPalette.colSecondaryContainer + + font { + family: Appearance?.font.family.sans ?? "sans-serif" + hintingPreference: Font.PreferFullHinting + pixelSize: Appearance?.font.size.normal ?? 15 + } +} diff --git a/Config/Config.qml b/Config/Config.qml index 4fc63dc..ef7b115 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -168,6 +168,7 @@ Singleton { return { logo: general.logo, wallpaperPath: general.wallpaperPath, + desktopIcons: general.desktopIcons, color: { wallust: general.color.wallust, mode: general.color.mode, diff --git a/Config/General.qml b/Config/General.qml index 9169d31..aac4cb1 100644 --- a/Config/General.qml +++ b/Config/General.qml @@ -6,6 +6,7 @@ JsonObject { } property Color color: Color { } + property bool desktopIcons: false property Idle idle: Idle { } property string logo: "" diff --git a/Helpers/AppSearch.qml b/Helpers/AppSearch.qml new file mode 100644 index 0000000..f5391bb --- /dev/null +++ b/Helpers/AppSearch.qml @@ -0,0 +1,122 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import "../scripts/levendist.js" as Levendist +import "../scripts/fuzzysort.js" as Fuzzy +import qs.Config + +Singleton { + id: root + + readonly property list list: Array.from(DesktopEntries.applications.values).sort((a, b) => a.name.localeCompare(b.name)) + readonly property var preppedNames: list.map(a => ({ + name: Fuzzy.prepare(`${a.name} `), + entry: a + })) + property var regexSubstitutions: [ + { + "regex": /^steam_app_(\d+)$/, + "replace": "steam_icon_$1" + }, + { + "regex": /Minecraft.*/, + "replace": "minecraft" + }, + { + "regex": /.*polkit.*/, + "replace": "system-lock-screen" + }, + { + "regex": /gcr.prompter/, + "replace": "system-lock-screen" + } + ] + property real scoreThreshold: 0.2 + property bool sloppySearch: Config.options?.search.sloppy ?? false + property var substitutions: ({ + "code-url-handler": "visual-studio-code", + "Code": "visual-studio-code", + "gnome-tweaks": "org.gnome.tweaks", + "pavucontrol-qt": "pavucontrol", + "wps": "wps-office2019-kprometheus", + "wpsoffice": "wps-office2019-kprometheus", + "footclient": "foot", + "zen": "zen-browser" + }) + + signal reload + + function computeScore(...args) { + return Levendist.computeScore(...args); + } + + function computeTextMatchScore(...args) { + return Levendist.computeTextMatchScore(...args); + } + + function fuzzyQuery(search: string): var { // Idk why list doesn't work + if (root.sloppySearch) { + const results = list.map(obj => ({ + entry: obj, + score: computeScore(obj.name.toLowerCase(), search.toLowerCase()) + })).filter(item => item.score > root.scoreThreshold).sort((a, b) => b.score - a.score); + return results.map(item => item.entry); + } + + return Fuzzy.go(search, preppedNames, { + all: true, + key: "name" + }).map(r => { + return r.obj.entry; + }); + } + + function guessIcon(str) { + if (!str || str.length == 0) + return "image-missing"; + + // Normal substitutions + if (substitutions[str]) + return substitutions[str]; + + // Regex substitutions + for (let i = 0; i < regexSubstitutions.length; i++) { + const substitution = regexSubstitutions[i]; + const replacedName = str.replace(substitution.regex, substitution.replace); + if (replacedName != str) + return replacedName; + } + + // If it gets detected normally, no need to guess + if (iconExists(str)) + return str; + + let guessStr = str; + // Guess: Take only app name of reverse domain name notation + guessStr = str.split('.').slice(-1)[0].toLowerCase(); + if (iconExists(guessStr)) + return guessStr; + // Guess: normalize to kebab case + guessStr = str.toLowerCase().replace(/\s+/g, "-"); + if (iconExists(guessStr)) + return guessStr; + // Guess: First fuzze desktop entry match + const searchResults = root.fuzzyQuery(str); + if (searchResults.length > 0) { + const firstEntry = searchResults[0]; + guessStr = firstEntry.icon; + if (iconExists(guessStr)) + return guessStr; + } + + // Give up + return str; + } + + function iconExists(iconName) { + if (!iconName || iconName.length == 0) + return false; + return (Quickshell.iconPath(iconName, true).length > 0) && !iconName.includes("image-missing"); + } +} diff --git a/Modules/DesktopIcons/BackgroundContextMenu.qml b/Modules/DesktopIcons/BackgroundContextMenu.qml index 0b96270..9d95d54 100644 --- a/Modules/DesktopIcons/BackgroundContextMenu.qml +++ b/Modules/DesktopIcons/BackgroundContextMenu.qml @@ -6,17 +6,18 @@ import Quickshell.Hyprland import qs.Components import qs.Config import qs.Paths +import qs.Helpers Item { id: root - + anchors.fill: parent z: 998 visible: false - + property real menuX: 0 property real menuY: 0 - + MouseArea { anchors.fill: parent onClicked: root.close() @@ -25,39 +26,46 @@ Item { CustomClippingRect { id: popupBackground readonly property real padding: 4 - + x: root.menuX y: root.menuY - + color: DynamicColors.tPalette.m3surface radius: Appearance.rounding.normal - + implicitWidth: menuLayout.implicitWidth + padding * 2 implicitHeight: menuLayout.implicitHeight + padding * 2 - + Behavior on opacity { Anim {} } opacity: root.visible ? 1 : 0 - + ColumnLayout { id: menuLayout - anchors.fill: parent - anchors.margins: popupBackground.padding + anchors.centerIn: parent spacing: 0 - StateLayer { - Layout.fillWidth: true - - contentItem: RowLayout { + CustomRect { + Layout.preferredWidth: 200 + radius: popupBackground.radius - popupBackground.padding + implicitHeight: openTerminalRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: openTerminalRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "terminal"; font.pointSize: 20 } CustomText { text: "Open terminal"; Layout.fillWidth: true } } - - onClicked: { - Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)]) - root.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)]) + root.close() + } } } @@ -69,81 +77,105 @@ Item { Layout.bottomMargin: 4 } - StateLayer { + CustomRect { Layout.fillWidth: true - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: settingsRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: settingsRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "settings"; font.pointSize: 20 } - CustomText { text: "Sleex settings"; Layout.fillWidth: true } + CustomText { text: "ZShell settings"; Layout.fillWidth: true } } - - onClicked: { - Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"]) - root.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"]) + root.close() + } } } CustomRect { Layout.fillWidth: true implicitHeight: 1 - color: Appearance.m3colors.m3outlineVariant + color: DynamicColors.palette.m3outlineVariant Layout.topMargin: 4 Layout.bottomMargin: 4 } - StateLayer { + CustomRect { Layout.fillWidth: true - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: logoutRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: logoutRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "logout"; font.pointSize: 20 } CustomText { text: "Logout"; Layout.fillWidth: true } } - - onClicked: { - Hyprland.dispatch("global quickshell:sessionOpen") - root.close() - } - } - CustomRect { - Layout.fillWidth: true - implicitHeight: 1 - color: Appearance.m3colors.m3outlineVariant - Layout.topMargin: 4 - Layout.bottomMargin: 4 - } - - StateLayer { - Layout.fillWidth: true - - contentItem: RowLayout { - spacing: 8 + StateLayer { anchors.fill: parent - anchors.margins: 12 - MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 } - CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true } - } - onClicked: { - Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons - root.close() + onClicked: { + Hyprland.dispatch("global quickshell:sessionOpen") + root.close() + } } } + + // CustomRect { + // Layout.fillWidth: true + // implicitHeight: 1 + // color: DynamicColors.palette.m3outlineVariant + // Layout.topMargin: 4 + // Layout.bottomMargin: 4 + // } + // + // CustomRect { + // Layout.fillWidth: true + // radius: popupBackground.radius - popupBackground.padding + // implicitHeight: desktopIconsRow.implicitHeight + Appearance.padding.small * 2 + // + // RowLayout { + // id: desktopIconsRow + // spacing: 8 + // anchors.fill: parent + // anchors.leftMargin: Appearance.padding.smaller + // + // MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 } + // CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true } + // } + // + // StateLayer { + // anchors.fill: parent + // + // onClicked: { + // Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons + // root.close() + // } + // } + // } } } - + function openAt(mouseX, mouseY, parentW, parentH) { - menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth) - menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight) + menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth)) + menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight)) visible = true } - + function close() { visible = false } diff --git a/Modules/DesktopIcons/DesktopIconContextMenu.qml b/Modules/DesktopIcons/DesktopIconContextMenu.qml index 238246f..e32cf9e 100644 --- a/Modules/DesktopIcons/DesktopIconContextMenu.qml +++ b/Modules/DesktopIcons/DesktopIconContextMenu.qml @@ -23,73 +23,87 @@ Item { property real menuX: 0 property real menuY: 0 - - MouseArea { - anchors.fill: parent - onClicked: contextMenu.close() - } CustomClippingRect { id: popupBackground - readonly property real padding: 4 - + readonly property real padding: Appearance.padding.small + x: contextMenu.menuX y: contextMenu.menuY - + color: DynamicColors.tPalette.m3surface radius: Appearance.rounding.normal - + implicitWidth: menuLayout.implicitWidth + padding * 2 implicitHeight: menuLayout.implicitHeight + padding * 2 - + Behavior on opacity { Anim {} } opacity: contextMenu.visible ? 1 : 0 - + ColumnLayout { id: menuLayout - anchors.fill: parent - anchors.margins: popupBackground.padding + anchors.centerIn: parent spacing: 0 - StateLayer { - Layout.fillWidth: true - - contentItem: RowLayout { + CustomRect { + Layout.preferredWidth: 160 + radius: popupBackground.radius - popupBackground.padding + implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: openRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "open_in_new"; font.pointSize: 20 } CustomText { text: "Open"; Layout.fillWidth: true } } - - onClicked: { - for (let i = 0; i < contextMenu.targetPaths.length; i++) { - let p = contextMenu.targetPaths[i]; - if (p === contextMenu.targetFilePath) { - if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute() - else contextMenu.openFileRequested(p, contextMenu.targetIsDir) - } else { - Quickshell.execDetached(["xdg-open", p]) + + StateLayer { + anchors.fill: parent + + onClicked: { + for (let i = 0; i < contextMenu.targetPaths.length; i++) { + let p = contextMenu.targetPaths[i]; + if (p === contextMenu.targetFilePath) { + if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute() + else contextMenu.openFileRequested(p, contextMenu.targetIsDir) + } else { + Quickshell.execDetached(["xdg-open", p]) + } } + contextMenu.close() } - contextMenu.close() } } - StateLayer { + CustomRect { Layout.fillWidth: true - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: openWithRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 } CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true } } - - onClicked: { - Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]) - contextMenu.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + if (contextMenu.targetIsDir) { + Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath]) + } else { + Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]) + } + contextMenu.close() + } } } @@ -101,92 +115,117 @@ Item { Layout.bottomMargin: 4 } - StateLayer { + CustomRect { Layout.fillWidth: true - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: copyPathRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "content_copy"; font.pointSize: 20 } CustomText { text: "Copy path"; Layout.fillWidth: true } } - - onClicked: { - Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]) - contextMenu.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]) + contextMenu.close() + } } } - StateLayer { + CustomRect { Layout.fillWidth: true visible: contextMenu.targetPaths.length === 1 - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: renameRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { text: "edit"; font.pointSize: 20 } CustomText { text: "Rename"; Layout.fillWidth: true } } - - onClicked: { - contextMenu.renameRequested(contextMenu.targetFilePath) - contextMenu.close() + + StateLayer { + anchors.fill: parent + + onClicked: { + contextMenu.renameRequested(contextMenu.targetFilePath) + contextMenu.close() + } } } Rectangle { Layout.fillWidth: true implicitHeight: 1 - color: Appearance.m3colors.m3outlineVariant + color: DynamicColors.palette.m3outlineVariant Layout.topMargin: 4 Layout.bottomMargin: 4 } - StateLayer { - id: deleteButton + CustomRect { Layout.fillWidth: true - colBackgroundHover: Appearance.colors.colError - - contentItem: RowLayout { + radius: popupBackground.radius - popupBackground.padding + implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: deleteRow spacing: 8 anchors.fill: parent - anchors.margins: 12 + anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { - text: "delete"; - font.pointSize: 20; - color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError + text: "delete" + font.pointSize: 20 + color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error } + CustomText { - text: "Move to trash"; - Layout.fillWidth: true; - color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError + text: "Move to trash" + Layout.fillWidth: true + color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error } } - - onClicked: { - let cmd = ["gio", "trash"].concat(contextMenu.targetPaths) - Quickshell.execDetached(cmd) - contextMenu.close() + + StateLayer { + id: deleteButton + anchors.fill: parent + color: DynamicColors.tPalette.m3error + + onClicked: { + let cmd = ["gio", "trash"].concat(contextMenu.targetPaths) + Quickshell.execDetached(cmd) + contextMenu.close() + } } } } } - + function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) { targetFilePath = path targetIsDir = isDir targetAppEntry = appEnt - + targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path] - - menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth) - menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight) - + + menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth)) + menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight)) + visible = true } - + function close() { visible = false } diff --git a/Modules/DesktopIcons/DesktopIconDelegate.qml b/Modules/DesktopIcons/DesktopIconDelegate.qml index 2ec681c..1d77c9b 100644 --- a/Modules/DesktopIcons/DesktopIconDelegate.qml +++ b/Modules/DesktopIcons/DesktopIconDelegate.qml @@ -15,6 +15,7 @@ Item { property int gridX: model.gridX property int gridY: model.gridY property bool isSnapping: snapAnimX.running || snapAnimY.running + property bool lassoActive property string resolvedIcon: { if (fileName.endsWith(".desktop")) { if (appEntry && appEntry.icon && appEntry.icon !== "") @@ -214,7 +215,7 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent - cursorShape: Qt.PointingHandCursor + cursorShape: root.lassoActive ? undefined : Qt.PointingHandCursor drag.target: dragContainer hoverEnabled: true diff --git a/Modules/DesktopIcons/DesktopIcons.qml b/Modules/DesktopIcons/DesktopIcons.qml index 03774b9..c06ba69 100644 --- a/Modules/DesktopIcons/DesktopIcons.qml +++ b/Modules/DesktopIcons/DesktopIcons.qml @@ -2,6 +2,8 @@ import QtQuick import Quickshell import qs.Modules import qs.Helpers +import qs.Config +import qs.Components import qs.Paths import ZShell.Services @@ -15,6 +17,7 @@ Item { property string editingFilePath: "" property real groupDragX: 0 property real groupDragY: 0 + property bool lassoActive: false property var selectedIcons: [] property real startX: 0 property real startY: 0 @@ -54,7 +57,6 @@ Item { root.groupDragY = 0; } - anchors.fill: parent focus: true Keys.onPressed: event => { @@ -68,15 +70,55 @@ Item { Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop)) } - Rectangle { + CustomRect { id: lasso - border.color: Appearance.colors.colPrimary + function hideLasso() { + fadeIn.stop(); + fadeOut.start(); + root.lassoActive = false; + } + + function showLasso() { + root.lassoActive = true; + fadeOut.stop(); + visible = true; + fadeIn.start(); + } + + border.color: DynamicColors.palette.m3primary border.width: 1 color: DynamicColors.tPalette.m3primary + opacity: 0 radius: Appearance.rounding.small visible: false z: 99 + + NumberAnimation { + id: fadeIn + + duration: 120 + from: 0 + property: "opacity" + target: lasso + to: 1 + } + + SequentialAnimation { + id: fadeOut + + NumberAnimation { + duration: 120 + from: lasso.opacity + property: "opacity" + target: lasso + to: 0 + } + + ScriptAction { + script: lasso.visible = false + } + } } MouseArea { @@ -85,10 +127,10 @@ Item { onPositionChanged: mouse => { if (lasso.visible) { - lasso.x = Math.min(mouse.x, root.startX); - lasso.y = Math.min(mouse.y, root.startY); - lasso.width = Math.abs(mouse.x - root.startX); - lasso.height = Math.abs(mouse.y - root.startY); + lasso.x = Math.floor(Math.min(mouse.x, root.startX)); + lasso.y = Math.floor(Math.min(mouse.y, root.startY)); + lasso.width = Math.floor(Math.abs(mouse.x - root.startX)); + lasso.height = Math.floor(Math.abs(mouse.y - root.startY)); let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth); let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth); @@ -115,17 +157,17 @@ Item { } else { bgContextMenu.close(); root.selectedIcons = []; - root.startX = mouse.x; - root.startY = mouse.y; - lasso.x = mouse.x; - lasso.y = mouse.y; + root.startX = Math.floor(mouse.x); + root.startY = Math.floor(mouse.y); + lasso.x = Math.floor(mouse.x); + lasso.y = Math.floor(mouse.y); lasso.width = 0; lasso.height = 0; - lasso.visible = true; + lasso.showLasso(); } } onReleased: { - lasso.visible = false; + lasso.hideLasso(); } } @@ -142,6 +184,8 @@ Item { delegate: DesktopIconDelegate { property int itemIndex: index + + lassoActive: root.lassoActive } } } diff --git a/Modules/Wallpaper/Wallpaper.qml b/Modules/Wallpaper/Wallpaper.qml index 06f940d..44c44d2 100644 --- a/Modules/Wallpaper/Wallpaper.qml +++ b/Modules/Wallpaper/Wallpaper.qml @@ -32,7 +32,14 @@ Loader { WallBackground { } - DesktopIcons { + Loader { + id: loader + + active: Config.general.desktopIcons + anchors.fill: parent + + sourceComponent: DesktopIcons { + } } } } diff --git a/Paths/Paths.qml b/Paths/Paths.qml index eacf7a7..27131f1 100644 --- a/Paths/Paths.qml +++ b/Paths/Paths.qml @@ -10,7 +10,7 @@ Singleton { readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell` readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell` readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell` - readonly property string desktop: `${Quickshell.env("XDG_DATA_HOME") || `${home}/Desktop`}` + readonly property string desktop: `${Quickshell.env("HOME")}/Desktop` readonly property string home: Quickshell.env("HOME") readonly property string imagecache: `${cache}/imagecache` readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell" diff --git a/scripts/levendist.js b/scripts/levendist.js new file mode 100644 index 0000000..332ed09 --- /dev/null +++ b/scripts/levendist.js @@ -0,0 +1,143 @@ +// Original code from https://github.com/koeqaife/hyprland-material-you +// Original code license: GPLv3 +// Translated to Js from Cython with an LLM and reviewed + +function min3(a, b, c) { + return a < b && a < c ? a : b < c ? b : c; +} + +function max3(a, b, c) { + return a > b && a > c ? a : b > c ? b : c; +} + +function min2(a, b) { + return a < b ? a : b; +} + +function max2(a, b) { + return a > b ? a : b; +} + +function levenshteinDistance(s1, s2) { + let len1 = s1.length; + let len2 = s2.length; + + if (len1 === 0) return len2; + if (len2 === 0) return len1; + + if (len2 > len1) { + [s1, s2] = [s2, s1]; + [len1, len2] = [len2, len1]; + } + + let prev = new Array(len2 + 1); + let curr = new Array(len2 + 1); + + for (let j = 0; j <= len2; j++) { + prev[j] = j; + } + + for (let i = 1; i <= len1; i++) { + curr[0] = i; + for (let j = 1; j <= len2; j++) { + let cost = s1[i - 1] === s2[j - 1] ? 0 : 1; + curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost); + } + [prev, curr] = [curr, prev]; + } + + return prev[len2]; +} + +function partialRatio(shortS, longS) { + let lenS = shortS.length; + let lenL = longS.length; + let best = 0.0; + + if (lenS === 0) return 1.0; + + for (let i = 0; i <= lenL - lenS; i++) { + let sub = longS.slice(i, i + lenS); + let dist = levenshteinDistance(shortS, sub); + let score = 1.0 - dist / lenS; + if (score > best) best = score; + } + + return best; +} + +function computeScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - dist / maxLen; + let part = + s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.85 * full + 0.15 * part; + + if (s1 && s2 && s1[0] !== s2[0]) { + score -= 0.05; + } + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 3) { + score -= (0.05 * lenDiff) / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.02 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.06; + } + + return Math.max(0.0, Math.min(1.0, score)); +} + +function computeTextMatchScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - dist / maxLen; + let part = + s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.4 * full + 0.6 * part; + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 10) { + score -= (0.02 * lenDiff) / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.01 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.2; + } + + return Math.max(0.0, Math.min(1.0, score)); +} -- 2.47.3 From 0b935a3096befc37cc9df82dfe2445aab1527b9c Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Thu, 12 Mar 2026 16:27:02 +0100 Subject: [PATCH 18/47] dock --- Config/Config.qml | 3 +- Config/DockConfig.qml | 1 + Drawers/Backgrounds.qml | 10 +++ Drawers/Interactions.qml | 6 ++ Drawers/Panels.qml | 12 ++++ Drawers/Windows.qml | 6 +- Helpers/TaskbarApps.qml | 86 ++++++++++++++++++++++++++ Modules/Dock/Content.qml | 28 +++++++-- Modules/Dock/Parts/DockAppButton.qml | 91 ++++++++++++++++++++++++++++ Modules/Dock/Parts/DockSeparator.qml | 12 ++++ 10 files changed, 247 insertions(+), 8 deletions(-) create mode 100644 Helpers/TaskbarApps.qml create mode 100644 Modules/Dock/Parts/DockAppButton.qml create mode 100644 Modules/Dock/Parts/DockSeparator.qml diff --git a/Config/Config.qml b/Config/Config.qml index ef7b115..4d0bcad 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -160,7 +160,8 @@ Singleton { hoverRegionHeight: dock.hoverRegionHeight, hoverToReveal: dock.hoverToReveal, pinnedApps: dock.pinnedApps, - pinnedOnStartup: dock.pinnedOnStartup + pinnedOnStartup: dock.pinnedOnStartup, + ignoredAppRegexes: dock.ignoredAppRegexes }; } diff --git a/Config/DockConfig.qml b/Config/DockConfig.qml index 135a3e0..36cc75d 100644 --- a/Config/DockConfig.qml +++ b/Config/DockConfig.qml @@ -5,6 +5,7 @@ JsonObject { property real height: 60 property real hoverRegionHeight: 2 property bool hoverToReveal: true + property list ignoredAppRegexes: [] property list pinnedApps: ["org.kde.dolphin", "kitty",] property bool pinnedOnStartup: false } diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index c7abccc..8c1fcef 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -13,6 +13,7 @@ import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources import qs.Modules.Drawing as Drawing import qs.Modules.Settings as Settings +import qs.Modules.Dock as Dock Shape { id: root @@ -24,6 +25,7 @@ Shape { anchors.fill: parent anchors.margins: Config.barConfig.border anchors.topMargin: bar.implicitHeight + asynchronous: true preferredRendererType: Shape.CurveRenderer Drawing.Background { @@ -93,4 +95,12 @@ Shape { startY: 0 wrapper: root.panels.settings } + + Dock.Background { + id: dock + + startX: (root.width - wrapper.width) / 2 - rounding + startY: root.height + wrapper: root.panels.dock + } } diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 25f3077..5116422 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -107,6 +107,9 @@ CustomMouseArea { } } + if (!visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y)) + visibilities.dock = true; + if (y < root.bar.implicitHeight) { root.bar.checkPopout(x); } @@ -145,6 +148,9 @@ CustomMouseArea { root.panels.osd.hovered = false; } } + + if (root.visibilities.launcher) + root.visibilities.dock = false; } function onOsdChanged() { diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 32b64f9..4aa5000 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -12,6 +12,7 @@ import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources import qs.Modules.Settings as Settings import qs.Modules.Drawing as Drawing +import qs.Modules.Dock as Dock import qs.Config Item { @@ -19,6 +20,7 @@ Item { required property Item bar readonly property alias dashboard: dashboard + readonly property alias dock: dock readonly property alias drawing: drawing required property Canvas drawingItem readonly property alias launcher: launcher @@ -143,4 +145,14 @@ Item { panels: root visibilities: root.visibilities } + + Dock.Wrapper { + id: dock + + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + panels: root + screen: root.screen + visibilities: root.visibilities + } } diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index a47e4a5..e38a84a 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -33,7 +33,7 @@ Variants { property var root: Quickshell.shellDir WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None color: "transparent" contentItem.focus: true mask: visibilities.isDrawing ? null : region @@ -94,7 +94,7 @@ Variants { HyprlandFocusGrab { id: focusGrab - active: visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) + active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) windows: [win] onCleared: { @@ -104,6 +104,7 @@ Variants { visibilities.osd = false; visibilities.settings = false; visibilities.resources = false; + visibilities.dock = false; panels.popouts.hasCurrent = false; } } @@ -113,6 +114,7 @@ Variants { property bool bar property bool dashboard + property bool dock property bool isDrawing property bool launcher property bool notif: NotifServer.popups.length > 0 diff --git a/Helpers/TaskbarApps.qml b/Helpers/TaskbarApps.qml new file mode 100644 index 0000000..4e19d3f --- /dev/null +++ b/Helpers/TaskbarApps.qml @@ -0,0 +1,86 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Config + +Singleton { + id: root + + property list apps: { + var map = new Map(); + + // Pinned apps + const pinnedApps = Config.dock.pinnedApps ?? []; + for (const appId of pinnedApps) { + if (!map.has(appId.toLowerCase())) + map.set(appId.toLowerCase(), ({ + pinned: true, + toplevels: [] + })); + } + + // Separator + if (pinnedApps.length > 0) { + map.set("SEPARATOR", { + pinned: false, + toplevels: [] + }); + } + + // Ignored apps + const ignoredRegexStrings = Config.dock.ignoredAppRegexes ?? []; + const ignoredRegexes = ignoredRegexStrings.map(pattern => new RegExp(pattern, "i")); + // Open windows + for (const toplevel of ToplevelManager.toplevels.values) { + if (ignoredRegexes.some(re => re.test(toplevel.appId))) + continue; + if (!map.has(toplevel.appId.toLowerCase())) + map.set(toplevel.appId.toLowerCase(), ({ + pinned: false, + toplevels: [] + })); + map.get(toplevel.appId.toLowerCase()).toplevels.push(toplevel); + } + + var values = []; + + for (const [key, value] of map) { + values.push(appEntryComp.createObject(null, { + appId: key, + toplevels: value.toplevels, + pinned: value.pinned + })); + } + + return values; + } + + function isPinned(appId) { + return Config.dock.pinnedApps.indexOf(appId) !== -1; + } + + function togglePin(appId) { + if (root.isPinned(appId)) { + Config.dock.pinnedApps = Config.dock.pinnedApps.filter(id => id !== appId); + } else { + Config.dock.pinnedApps = Config.dock.pinnedApps.concat([appId]); + } + } + + Component { + id: appEntryComp + + TaskbarAppEntry { + } + } + + component TaskbarAppEntry: QtObject { + id: wrapper + + required property string appId + required property bool pinned + required property list toplevels + } +} diff --git a/Modules/Dock/Content.qml b/Modules/Dock/Content.qml index 6089700..2b28f55 100644 --- a/Modules/Dock/Content.qml +++ b/Modules/Dock/Content.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick +import qs.Modules.Dock.Parts import qs.Components import qs.Helpers import qs.Config @@ -17,12 +18,29 @@ Item { implicitHeight: Config.dock.height + root.padding * 2 implicitWidth: dockRow.implicitWidth + root.padding * 2 - RowLayout { + CustomListView { id: dockRow - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: Appearance.spacing.small + anchors.centerIn: parent + implicitHeight: Config.dock.height + implicitWidth: contentWidth + orientation: ListView.Horizontal + spacing: Appearance.padding.smaller + + delegate: DockAppButton { + required property var modelData + + appListRoot: root + appToplevel: modelData + visibilities: root.visibilities + } + Behavior on implicitWidth { + Anim { + } + } + model: ScriptModel { + objectProp: "appId" + values: TaskbarApps.apps + } } } diff --git a/Modules/Dock/Parts/DockAppButton.qml b/Modules/Dock/Parts/DockAppButton.qml new file mode 100644 index 0000000..98b1972 --- /dev/null +++ b/Modules/Dock/Parts/DockAppButton.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs.Components +import qs.Helpers +import qs.Config + +CustomRect { + id: root + + property bool appIsActive: appToplevel.toplevels.find(t => (t.activated == true)) !== undefined + property var appListRoot + property var appToplevel + property real countDotHeight: 4 + property real countDotWidth: 10 + property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel.appId) + property real iconSize: implicitHeight - 20 + readonly property bool isSeparator: appToplevel.appId === "SEPARATOR" + property int lastFocused: -1 + required property PersistentProperties visibilities + + implicitHeight: Config.dock.height + implicitWidth: isSeparator ? 1 : implicitHeight + radius: Appearance.rounding.normal - Appearance.padding.small + + Loader { + active: !isSeparator + anchors.centerIn: parent + + sourceComponent: ColumnLayout { + IconImage { + id: icon + + Layout.alignment: Qt.AlignHCenter + implicitSize: root.iconSize + source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing") + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 3 + + Repeater { + model: Math.min(appToplevel.toplevels.length, 3) + + delegate: Rectangle { + required property int index + + color: appIsActive ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3primary + implicitHeight: root.countDotHeight + implicitWidth: (appToplevel.toplevels.length <= 3) ? root.countDotWidth : root.countDotHeight // Circles when too many + radius: Appearance.rounding.full + } + } + } + } + } + + StateLayer { + onClicked: { + if (appToplevel.toplevels.length === 0) { + root.desktopEntry?.execute(); + root.visibilities.dock = false; + return; + } + lastFocused = (lastFocused + 1) % appToplevel.toplevels.length; + appToplevel.toplevels[lastFocused].activate(); + root.visibilities.dock = false; + } + } + + Connections { + function onApplicationsChanged() { + root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel.appId); + } + + target: DesktopEntries + } + + Loader { + active: isSeparator + + sourceComponent: DockSeparator { + } + + anchors { + fill: parent + } + } +} diff --git a/Modules/Dock/Parts/DockSeparator.qml b/Modules/Dock/Parts/DockSeparator.qml new file mode 100644 index 0000000..b535412 --- /dev/null +++ b/Modules/Dock/Parts/DockSeparator.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + Layout.bottomMargin: dockRow.padding + Appearance.rounding.normal + Layout.fillHeight: true + Layout.topMargin: dockRow.padding + Appearance.rounding.normal + color: DynamicColors.palette.m3outlineVariant + implicitWidth: 1 +} -- 2.47.3 From 0cd2b3dbfc4b7bf136c36f9037084ef09941c014 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Thu, 12 Mar 2026 16:30:53 +0100 Subject: [PATCH 19/47] dock --- Config/Config.qml | 1 - Config/DockConfig.qml | 1 - 2 files changed, 2 deletions(-) diff --git a/Config/Config.qml b/Config/Config.qml index 4d0bcad..0f7d41d 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -157,7 +157,6 @@ Singleton { return { enable: dock.enable, height: dock.height, - hoverRegionHeight: dock.hoverRegionHeight, hoverToReveal: dock.hoverToReveal, pinnedApps: dock.pinnedApps, pinnedOnStartup: dock.pinnedOnStartup, diff --git a/Config/DockConfig.qml b/Config/DockConfig.qml index 36cc75d..eeaf1de 100644 --- a/Config/DockConfig.qml +++ b/Config/DockConfig.qml @@ -3,7 +3,6 @@ import Quickshell.Io JsonObject { property bool enable: false property real height: 60 - property real hoverRegionHeight: 2 property bool hoverToReveal: true property list ignoredAppRegexes: [] property list pinnedApps: ["org.kde.dolphin", "kitty",] -- 2.47.3 From 42c4b663998716ec053ff03db6debd4ba62e9676 Mon Sep 17 00:00:00 2001 From: Aram Markarov Date: Thu, 12 Mar 2026 17:39:10 +0100 Subject: [PATCH 20/47] comments removed from a app2unit.nix file. --- nix/app2unit.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/app2unit.nix b/nix/app2unit.nix index ce5fee2..a29a66a 100644 --- a/nix/app2unit.nix +++ b/nix/app2unit.nix @@ -1,11 +1,11 @@ { - pkgs, # To ensure the nixpkgs version of app2unit + pkgs, fetchFromGitHub, ... }: pkgs.app2unit.overrideAttrs ( final: prev: rec { - version = "1.0.3"; # Fix old issue related to missing env var + version = "1.0.3"; src = fetchFromGitHub { owner = "Vladimir-csp"; repo = "app2unit"; -- 2.47.3 From 042a92a8819ca49d3a94869f969cb49a5273a55d Mon Sep 17 00:00:00 2001 From: Aram Markarov Date: Thu, 12 Mar 2026 17:55:42 +0100 Subject: [PATCH 21/47] added result folder (nix build file) to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dc709e6..3fbbd29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.result/ .pyre/ .cache/ .venv/ -- 2.47.3 From 08b18880a0889fcba993ae75f0f02305284e858a Mon Sep 17 00:00:00 2001 From: Aram Markarov Date: Thu, 12 Mar 2026 18:50:29 +0100 Subject: [PATCH 22/47] added result folder (nix build file) to gitignore(updated to work) --- .gitignore | 2 +- result | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 120000 result diff --git a/.gitignore b/.gitignore index 3fbbd29..295910b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.result/ +.result* .pyre/ .cache/ .venv/ diff --git a/result b/result new file mode 120000 index 0000000..56ae149 --- /dev/null +++ b/result @@ -0,0 +1 @@ +/nix/store/dgf42nmv557p8lg0kv1g0cwjk8cnvalx-zshell-1.0.0 \ No newline at end of file -- 2.47.3 From b65117e213b613d9f37749b41e2aa9f9c4d41bf8 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Thu, 12 Mar 2026 19:24:23 +0100 Subject: [PATCH 23/47] dock --- Config/Config.qml | 4 + Helpers/TaskbarApps.qml | 106 ++++++---- .../DesktopIcons/BackgroundContextMenu.qml | 3 +- Modules/Dock/Content.qml | 192 ++++++++++++++++-- Modules/Dock/Parts/DockAppButton.qml | 20 +- 5 files changed, 254 insertions(+), 71 deletions(-) diff --git a/Config/Config.qml b/Config/Config.qml index 0f7d41d..e3b4776 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -33,6 +33,10 @@ Singleton { recentSaveCooldown.restart(); } + function saveNoToast(): void { + saveTimer.restart(); + } + function serializeAppearance(): var { return { rounding: { diff --git a/Helpers/TaskbarApps.qml b/Helpers/TaskbarApps.qml index 4e19d3f..6bed8ee 100644 --- a/Helpers/TaskbarApps.qml +++ b/Helpers/TaskbarApps.qml @@ -9,64 +9,82 @@ Singleton { id: root property list apps: { - var map = new Map(); + const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId)); + const openMap = buildOpenMap(); + const openIds = [...openMap.keys()]; + const sessionOrder = uniq(root.unpinnedOrder.map(normalizeId)); - // Pinned apps - const pinnedApps = Config.dock.pinnedApps ?? []; - for (const appId of pinnedApps) { - if (!map.has(appId.toLowerCase())) - map.set(appId.toLowerCase(), ({ - pinned: true, - toplevels: [] - })); - } + const orderedUnpinned = sessionOrder.filter(id => openIds.includes(id) && !pinnedApps.includes(id)).concat(openIds.filter(id => !pinnedApps.includes(id) && !sessionOrder.includes(id))); - // Separator - if (pinnedApps.length > 0) { - map.set("SEPARATOR", { + return [].concat(pinnedApps.map(appId => appEntryComp.createObject(null, { + appId, + pinned: true, + toplevels: openMap.get(appId) ?? [] + }))).concat(pinnedApps.length > 0 ? [appEntryComp.createObject(null, { + appId: root.separatorId, pinned: false, toplevels: [] - }); - } + })] : []).concat(orderedUnpinned.map(appId => appEntryComp.createObject(null, { + appId, + pinned: false, + toplevels: openMap.get(appId) ?? [] + }))); + } + readonly property string separatorId: "__dock_separator__" + property var unpinnedOrder: [] - // Ignored apps - const ignoredRegexStrings = Config.dock.ignoredAppRegexes ?? []; - const ignoredRegexes = ignoredRegexStrings.map(pattern => new RegExp(pattern, "i")); - // Open windows - for (const toplevel of ToplevelManager.toplevels.values) { + function buildOpenMap() { + const ignoredRegexes = (Config.dock.ignoredAppRegexes ?? []).map(pattern => new RegExp(pattern, "i")); + + return ToplevelManager.toplevels.values.reduce((map, toplevel) => { if (ignoredRegexes.some(re => re.test(toplevel.appId))) - continue; - if (!map.has(toplevel.appId.toLowerCase())) - map.set(toplevel.appId.toLowerCase(), ({ - pinned: false, - toplevels: [] - })); - map.get(toplevel.appId.toLowerCase()).toplevels.push(toplevel); - } + return map; - var values = []; + const appId = normalizeId(toplevel.appId); + if (!appId) + return map; - for (const [key, value] of map) { - values.push(appEntryComp.createObject(null, { - appId: key, - toplevels: value.toplevels, - pinned: value.pinned - })); - } + map.set(appId, (map.get(appId) ?? []).concat([toplevel])); + return map; + }, new Map()); + } - return values; + function commitVisualOrder(ids) { + const orderedIds = uniq(ids.map(normalizeId)); + const separatorIndex = orderedIds.indexOf(root.separatorId); + + const pinnedApps = (separatorIndex === -1 ? [] : orderedIds.slice(0, separatorIndex)).filter(id => id !== root.separatorId); + + const visibleUnpinned = orderedIds.slice(separatorIndex === -1 ? 0 : separatorIndex + 1).filter(id => id !== root.separatorId); + + Config.dock.pinnedApps = pinnedApps; + root.unpinnedOrder = visibleUnpinned.concat(root.unpinnedOrder.map(normalizeId).filter(id => !pinnedApps.includes(id) && !visibleUnpinned.includes(id))); + Config.saveNoToast(); } function isPinned(appId) { - return Config.dock.pinnedApps.indexOf(appId) !== -1; + return uniq((Config.dock.pinnedApps ?? []).map(normalizeId)).includes(normalizeId(appId)); + } + + function normalizeId(appId) { + if (appId === root.separatorId) + return root.separatorId; + + return String(appId ?? "").toLowerCase(); } function togglePin(appId) { - if (root.isPinned(appId)) { - Config.dock.pinnedApps = Config.dock.pinnedApps.filter(id => id !== appId); - } else { - Config.dock.pinnedApps = Config.dock.pinnedApps.concat([appId]); - } + const id = normalizeId(appId); + const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId)); + const pinned = pinnedApps.includes(id); + + Config.dock.pinnedApps = pinned ? pinnedApps.filter(x => x !== id) : pinnedApps.concat([id]); + + root.unpinnedOrder = pinned ? [id].concat(root.unpinnedOrder.map(normalizeId).filter(x => x !== id)) : root.unpinnedOrder.map(normalizeId).filter(x => x !== id); + } + + function uniq(ids) { + return (ids ?? []).filter((id, i, arr) => id && arr.indexOf(id) === i); } Component { @@ -77,8 +95,6 @@ Singleton { } component TaskbarAppEntry: QtObject { - id: wrapper - required property string appId required property bool pinned required property list toplevels diff --git a/Modules/DesktopIcons/BackgroundContextMenu.qml b/Modules/DesktopIcons/BackgroundContextMenu.qml index 9d95d54..a9eefa8 100644 --- a/Modules/DesktopIcons/BackgroundContextMenu.qml +++ b/Modules/DesktopIcons/BackgroundContextMenu.qml @@ -96,7 +96,8 @@ Item { anchors.fill: parent onClicked: { - Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"]) + const visibilities = Visibilities.getForActive(); + visibilities.settings = true; root.close() } } diff --git a/Modules/Dock/Content.qml b/Modules/Dock/Content.qml index 2b28f55..2a4bba4 100644 --- a/Modules/Dock/Content.qml +++ b/Modules/Dock/Content.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick +import QtQml.Models import qs.Modules.Dock.Parts import qs.Components import qs.Helpers @@ -10,37 +11,198 @@ import qs.Config Item { id: root + readonly property int dockContentWidth: TaskbarApps.apps.reduce((sum, app, i) => sum + (app.appId === TaskbarApps.separatorId ? 1 : Config.dock.height) + (i > 0 ? dockRow.spacing : 0), 0) + property bool dragActive: false + property real dragHeight: Config.dock.height + property real dragStartX: 0 + property real dragStartY: 0 + property real dragWidth: Config.dock.height + property real dragX: 0 + property real dragY: 0 + property string draggedAppId: "" + property var draggedModelData: null readonly property int padding: Appearance.padding.small required property var panels readonly property int rounding: Appearance.rounding.large required property PersistentProperties visibilities + property var visualIds: [] + + function beginVisualDrag(appId, modelData, item) { + const pos = item.mapToItem(root, 0, 0); + + root.visualIds = TaskbarApps.apps.map(app => app.appId); + root.draggedAppId = appId; + root.draggedModelData = modelData; + root.dragWidth = item.width; + root.dragHeight = item.height; + root.dragStartX = pos.x; + root.dragStartY = pos.y; + root.dragX = pos.x; + root.dragY = pos.y; + root.dragActive = true; + } + + function endVisualDrag() { + const ids = root.visualIds.slice(); + + root.dragActive = false; + root.draggedAppId = ""; + root.draggedModelData = null; + root.visualIds = []; + + TaskbarApps.commitVisualOrder(ids); + } + + function moveArrayItem(list, from, to) { + const next = list.slice(); + const [item] = next.splice(from, 1); + next.splice(to, 0, item); + return next; + } + + function previewVisualMove(from, hovered, before) { + let to = hovered + (before ? 0 : 1); + + if (to > from) + to -= 1; + + to = Math.max(0, Math.min(visualModel.items.count - 1, to)); + + if (from === to) + return; + + visualModel.items.move(from, to); + root.visualIds = moveArrayItem(root.visualIds, from, to); + } implicitHeight: Config.dock.height + root.padding * 2 - implicitWidth: dockRow.implicitWidth + root.padding * 2 + implicitWidth: root.dockContentWidth + root.padding * 2 - CustomListView { - id: dockRow + Component { + id: dockDelegate - anchors.centerIn: parent - implicitHeight: Config.dock.height - implicitWidth: contentWidth - orientation: ListView.Horizontal - spacing: Appearance.padding.smaller + DropArea { + id: slot - delegate: DockAppButton { + readonly property string appId: modelData.appId + readonly property bool isSeparator: appId === TaskbarApps.separatorId required property var modelData - appListRoot: root - appToplevel: modelData - visibilities: root.visibilities - } - Behavior on implicitWidth { - Anim { + function previewReorder(drag) { + const source = drag.source; + if (!source || !source.appId || source.appId === appId) + return; + + const from = source.visualIndex; + const hovered = slot.DelegateModel.itemsIndex; + + if (from < 0 || hovered < 0) + return; + + root.previewVisualMove(from, hovered, drag.x < width / 2); + } + + height: Config.dock.height + width: isSeparator ? 1 : Config.dock.height + + onEntered: drag => previewReorder(drag) + onPositionChanged: drag => previewReorder(drag) + + DockAppButton { + id: button + + anchors.centerIn: parent + appListRoot: root + appToplevel: modelData + visibilities: root.visibilities + visible: root.draggedAppId !== slot.appId + } + + DragHandler { + id: dragHandler + + enabled: !slot.isSeparator + grabPermissions: PointerHandler.CanTakeOverFromAnything + target: null + xAxis.enabled: true + yAxis.enabled: false + + onActiveChanged: { + if (active) { + root.beginVisualDrag(slot.appId, slot.modelData, button); + } else if (root.draggedAppId === slot.appId) { + dragProxy.Drag.drop(); + root.endVisualDrag(); + } + } + onActiveTranslationChanged: { + if (!active || root.draggedAppId !== slot.appId) + return; + + root.dragX = root.dragStartX + activeTranslation.x; + root.dragY = root.dragStartY + activeTranslation.y; + } } } + } + + DelegateModel { + id: visualModel + + delegate: dockDelegate + model: ScriptModel { objectProp: "appId" values: TaskbarApps.apps } } + + CustomListView { + id: dockRow + + anchors.centerIn: parent + boundsBehavior: Flickable.StopAtBounds + implicitHeight: Config.dock.height + implicitWidth: root.dockContentWidth + interactive: !root.dragActive + model: visualModel + orientation: ListView.Horizontal + spacing: Appearance.padding.smaller + + Behavior on implicitWidth { + Anim { + } + } + moveDisplaced: Transition { + Anim { + properties: "x,y" + } + } + } + + Item { + id: dragProxy + + property string appId: root.draggedAppId + property int visualIndex: root.visualIds.indexOf(root.draggedAppId) + + Drag.active: root.dragActive + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + Drag.source: dragProxy + height: root.dragHeight + visible: root.dragActive && !!root.draggedModelData + width: root.dragWidth + x: root.dragX + y: root.dragY + z: 9999 + + DockAppButton { + anchors.fill: parent + appListRoot: root + appToplevel: root.draggedModelData + enabled: false + visibilities: root.visibilities + } + } } diff --git a/Modules/Dock/Parts/DockAppButton.qml b/Modules/Dock/Parts/DockAppButton.qml index 98b1972..c7e79b9 100644 --- a/Modules/Dock/Parts/DockAppButton.qml +++ b/Modules/Dock/Parts/DockAppButton.qml @@ -9,14 +9,14 @@ import qs.Config CustomRect { id: root - property bool appIsActive: appToplevel.toplevels.find(t => (t.activated == true)) !== undefined + property bool appIsActive: appToplevel?.toplevels.find(t => (t.activated == true)) !== undefined property var appListRoot property var appToplevel property real countDotHeight: 4 property real countDotWidth: 10 - property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel.appId) + property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel?.appId) property real iconSize: implicitHeight - 20 - readonly property bool isSeparator: appToplevel.appId === "SEPARATOR" + readonly property bool isSeparator: appToplevel?.appId === "__dock_separator__" property int lastFocused: -1 required property PersistentProperties visibilities @@ -34,7 +34,7 @@ CustomRect { Layout.alignment: Qt.AlignHCenter implicitSize: root.iconSize - source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing") + source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel?.appId), "image-missing") } RowLayout { @@ -42,14 +42,14 @@ CustomRect { spacing: 3 Repeater { - model: Math.min(appToplevel.toplevels.length, 3) + model: Math.min(appToplevel?.toplevels.length, 3) delegate: Rectangle { required property int index color: appIsActive ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3primary implicitHeight: root.countDotHeight - implicitWidth: (appToplevel.toplevels.length <= 3) ? root.countDotWidth : root.countDotHeight // Circles when too many + implicitWidth: (appToplevel?.toplevels.length <= 3) ? root.countDotWidth : root.countDotHeight // Circles when too many radius: Appearance.rounding.full } } @@ -59,20 +59,20 @@ CustomRect { StateLayer { onClicked: { - if (appToplevel.toplevels.length === 0) { + if (appToplevel?.toplevels.length === 0) { root.desktopEntry?.execute(); root.visibilities.dock = false; return; } - lastFocused = (lastFocused + 1) % appToplevel.toplevels.length; - appToplevel.toplevels[lastFocused].activate(); + lastFocused = (lastFocused + 1) % appToplevel?.toplevels.length; + appToplevel?.toplevels[lastFocused].activate(); root.visibilities.dock = false; } } Connections { function onApplicationsChanged() { - root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel.appId); + root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel?.appId); } target: DesktopEntries -- 2.47.3 From 37e482a361f77be1b12dfce85a81b4fcd11f8c5e Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Fri, 13 Mar 2026 14:03:40 +0100 Subject: [PATCH 24/47] dock --- Helpers/AppSearch.qml | 134 +++++++++++++++++++++++---------------- Helpers/TaskbarApps.qml | 37 +++++------ Modules/Bar/Border.qml | 2 +- Modules/Dock/Content.qml | 108 ++++++++++++++++++++++++++++--- Modules/Dock/Wrapper.qml | 9 ++- 5 files changed, 208 insertions(+), 82 deletions(-) diff --git a/Helpers/AppSearch.qml b/Helpers/AppSearch.qml index f5391bb..400b3d9 100644 --- a/Helpers/AppSearch.qml +++ b/Helpers/AppSearch.qml @@ -9,7 +9,11 @@ import qs.Config Singleton { id: root - readonly property list list: Array.from(DesktopEntries.applications.values).sort((a, b) => a.name.localeCompare(b.name)) + readonly property list list: Array.from(DesktopEntries.applications.values).filter((app, index, self) => index === self.findIndex(t => (t.id === app.id))) + readonly property var preppedIcons: list.map(a => ({ + name: Fuzzy.prepare(`${a.icon} `), + entry: a + })) readonly property var preppedNames: list.map(a => ({ name: Fuzzy.prepare(`${a.name} `), entry: a @@ -32,8 +36,8 @@ Singleton { "replace": "system-lock-screen" } ] - property real scoreThreshold: 0.2 - property bool sloppySearch: Config.options?.search.sloppy ?? false + readonly property real scoreGapThreshold: 0.1 + readonly property real scoreThreshold: 0.6 property var substitutions: ({ "code-url-handler": "visual-studio-code", "Code": "visual-studio-code", @@ -41,46 +45,65 @@ Singleton { "pavucontrol-qt": "pavucontrol", "wps": "wps-office2019-kprometheus", "wpsoffice": "wps-office2019-kprometheus", - "footclient": "foot", - "zen": "zen-browser" + "footclient": "foot" }) - signal reload - - function computeScore(...args) { - return Levendist.computeScore(...args); - } - - function computeTextMatchScore(...args) { - return Levendist.computeTextMatchScore(...args); - } - - function fuzzyQuery(search: string): var { // Idk why list doesn't work - if (root.sloppySearch) { - const results = list.map(obj => ({ - entry: obj, - score: computeScore(obj.name.toLowerCase(), search.toLowerCase()) - })).filter(item => item.score > root.scoreThreshold).sort((a, b) => b.score - a.score); - return results.map(item => item.entry); - } - - return Fuzzy.go(search, preppedNames, { - all: true, - key: "name" - }).map(r => { - return r.obj.entry; + function bestFuzzyEntry(search: string, preppedList: list, key: string): var { + const results = Fuzzy.go(search, preppedList, { + key: key, + threshold: root.scoreThreshold, + limit: 2 }); + + if (!results || results.length === 0) + return null; + + const best = results[0]; + const second = results.length > 1 ? results[1] : null; + + if (second && (best.score - second.score) < root.scoreGapThreshold) + return null; + + return best.obj.entry; + } + + function fuzzyQuery(search: string, preppedList: list): var { + const entry = bestFuzzyEntry(search, preppedList, "name"); + return entry ? [entry] : []; + } + + function getKebabNormalizedAppName(str: string): string { + return str.toLowerCase().replace(/\s+/g, "-"); + } + + function getReverseDomainNameAppName(str: string): string { + return str.split('.').slice(-1)[0]; + } + + function getUndescoreToKebabAppName(str: string): string { + return str.toLowerCase().replace(/_/g, "-"); } function guessIcon(str) { if (!str || str.length == 0) return "image-missing"; - // Normal substitutions + if (iconExists(str)) + return str; + + const entry = DesktopEntries.byId(str); + if (entry) + return entry.icon; + + const heuristicEntry = DesktopEntries.heuristicLookup(str); + if (heuristicEntry) + return heuristicEntry.icon; + if (substitutions[str]) return substitutions[str]; + if (substitutions[str.toLowerCase()]) + return substitutions[str.toLowerCase()]; - // Regex substitutions for (let i = 0; i < regexSubstitutions.length; i++) { const substitution = regexSubstitutions[i]; const replacedName = str.replace(substitution.regex, substitution.replace); @@ -88,30 +111,35 @@ Singleton { return replacedName; } - // If it gets detected normally, no need to guess - if (iconExists(str)) - return str; + const lowercased = str.toLowerCase(); + if (iconExists(lowercased)) + return lowercased; - let guessStr = str; - // Guess: Take only app name of reverse domain name notation - guessStr = str.split('.').slice(-1)[0].toLowerCase(); - if (iconExists(guessStr)) - return guessStr; - // Guess: normalize to kebab case - guessStr = str.toLowerCase().replace(/\s+/g, "-"); - if (iconExists(guessStr)) - return guessStr; - // Guess: First fuzze desktop entry match - const searchResults = root.fuzzyQuery(str); - if (searchResults.length > 0) { - const firstEntry = searchResults[0]; - guessStr = firstEntry.icon; - if (iconExists(guessStr)) - return guessStr; - } + const reverseDomainNameAppName = getReverseDomainNameAppName(str); + if (iconExists(reverseDomainNameAppName)) + return reverseDomainNameAppName; - // Give up - return str; + const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase(); + if (iconExists(lowercasedDomainNameAppName)) + return lowercasedDomainNameAppName; + + const kebabNormalizedGuess = getKebabNormalizedAppName(str); + if (iconExists(kebabNormalizedGuess)) + return kebabNormalizedGuess; + + const undescoreToKebabGuess = getUndescoreToKebabAppName(str); + if (iconExists(undescoreToKebabGuess)) + return undescoreToKebabGuess; + + const iconSearchResult = fuzzyQuery(str, preppedIcons); + if (iconSearchResult && iconExists(iconSearchResult.icon)) + return iconSearchResult.icon; + + const nameSearchResult = root.fuzzyQuery(str, preppedNames); + if (nameSearchResult && iconExists(nameSearchResult.icon)) + return nameSearchResult.icon; + + return "application-x-executable"; } function iconExists(iconName) { diff --git a/Helpers/TaskbarApps.qml b/Helpers/TaskbarApps.qml index 6bed8ee..8f3f78b 100644 --- a/Helpers/TaskbarApps.qml +++ b/Helpers/TaskbarApps.qml @@ -8,7 +8,7 @@ import qs.Config Singleton { id: root - property list apps: { + property var apps: { const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId)); const openMap = buildOpenMap(); const openIds = [...openMap.keys()]; @@ -16,19 +16,33 @@ Singleton { const orderedUnpinned = sessionOrder.filter(id => openIds.includes(id) && !pinnedApps.includes(id)).concat(openIds.filter(id => !pinnedApps.includes(id) && !sessionOrder.includes(id))); - return [].concat(pinnedApps.map(appId => appEntryComp.createObject(null, { + const out = []; + + for (const appId of pinnedApps) { + out.push({ appId, pinned: true, toplevels: openMap.get(appId) ?? [] - }))).concat(pinnedApps.length > 0 ? [appEntryComp.createObject(null, { + }); + } + + if (pinnedApps.length > 0) { + out.push({ appId: root.separatorId, pinned: false, toplevels: [] - })] : []).concat(orderedUnpinned.map(appId => appEntryComp.createObject(null, { + }); + } + + for (const appId of orderedUnpinned) { + out.push({ appId, pinned: false, toplevels: openMap.get(appId) ?? [] - }))); + }); + } + + return out; } readonly property string separatorId: "__dock_separator__" property var unpinnedOrder: [] @@ -86,17 +100,4 @@ Singleton { function uniq(ids) { return (ids ?? []).filter((id, i, arr) => id && arr.indexOf(id) === i); } - - Component { - id: appEntryComp - - TaskbarAppEntry { - } - } - - component TaskbarAppEntry: QtObject { - required property string appId - required property bool pinned - required property list toplevels - } } diff --git a/Modules/Bar/Border.qml b/Modules/Bar/Border.qml index f82496f..d12523a 100644 --- a/Modules/Bar/Border.qml +++ b/Modules/Bar/Border.qml @@ -17,7 +17,7 @@ Item { CustomRect { anchors.fill: parent - color: Config.barConfig.border === 1 ? "transparent" : DynamicColors.palette.m3surface + color: !root.bar.isHovered && Config.barConfig.autoHide ? "transparent" : DynamicColors.palette.m3surface layer.enabled: true layer.effect: MultiEffect { diff --git a/Modules/Dock/Content.qml b/Modules/Dock/Content.qml index 2a4bba4..955d955 100644 --- a/Modules/Dock/Content.qml +++ b/Modules/Dock/Content.qml @@ -24,6 +24,7 @@ Item { readonly property int padding: Appearance.padding.small required property var panels readonly property int rounding: Appearance.rounding.large + required property ShellScreen screen required property PersistentProperties visibilities property var visualIds: [] @@ -87,6 +88,7 @@ Item { readonly property string appId: modelData.appId readonly property bool isSeparator: appId === TaskbarApps.separatorId required property var modelData + property bool removing: false function previewReorder(drag) { const source = drag.source; @@ -102,12 +104,52 @@ Item { root.previewVisualMove(from, hovered, drag.x < width / 2); } - height: Config.dock.height - width: isSeparator ? 1 : Config.dock.height + function startDetachedRemove() { + const p = mapToItem(removalLayer, 0, 0); + removing = true; + ListView.delayRemove = true; + + parent = removalLayer; + x = p.x; + y = p.y; + + removeAnim.start(); + } + + height: Config.dock.height + transformOrigin: Item.Center + width: isSeparator ? 1 : Config.dock.height + z: removing ? 1 : 0 + + ListView.onRemove: startDetachedRemove() onEntered: drag => previewReorder(drag) onPositionChanged: drag => previewReorder(drag) + SequentialAnimation { + id: removeAnim + + ParallelAnimation { + Anim { + property: "opacity" + target: slot + to: 0 + } + + Anim { + property: "scale" + target: slot + to: 0.5 + } + } + + ScriptAction { + script: { + slot.ListView.delayRemove = false; + } + } + } + DockAppButton { id: button @@ -121,7 +163,7 @@ Item { DragHandler { id: dragHandler - enabled: !slot.isSeparator + enabled: !slot.isSeparator && !slot.removing grabPermissions: PointerHandler.CanTakeOverFromAnything target: null xAxis.enabled: true @@ -160,24 +202,72 @@ Item { CustomListView { id: dockRow - anchors.centerIn: parent + property bool enableAddAnimation: false + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.top: parent.top boundsBehavior: Flickable.StopAtBounds - implicitHeight: Config.dock.height - implicitWidth: root.dockContentWidth + height: Config.dock.height + implicitWidth: root.dockContentWidth + Config.dock.height interactive: !root.dragActive model: visualModel orientation: ListView.Horizontal spacing: Appearance.padding.smaller - Behavior on implicitWidth { - Anim { + add: Transition { + ParallelAnimation { + Anim { + duration: dockRow.enableAddAnimation ? Appearance.anim.durations.normal : 0 + from: 0 + property: "opacity" + to: 1 + } + + Anim { + duration: dockRow.enableAddAnimation ? Appearance.anim.durations.normal : 0 + from: 0.5 + property: "scale" + to: 1 + } } } - moveDisplaced: Transition { + displaced: Transition { Anim { + duration: Appearance.anim.durations.small properties: "x,y" } } + move: Transition { + Anim { + duration: Appearance.anim.durations.small + properties: "x,y" + } + } + remove: Transition { + ParallelAnimation { + Anim { + property: "opacity" + to: 0 + } + + Anim { + property: "scale" + to: 0.5 + } + } + } + + Component.onCompleted: { + Qt.callLater(() => enableAddAnimation = true); + } + } + + Item { + id: removalLayer + + anchors.fill: parent + z: 9998 } Item { diff --git a/Modules/Dock/Wrapper.qml b/Modules/Dock/Wrapper.qml index 5d7e36d..667ab62 100644 --- a/Modules/Dock/Wrapper.qml +++ b/Modules/Dock/Wrapper.qml @@ -18,6 +18,12 @@ Item { implicitWidth: content.implicitWidth visible: height > 0 + Behavior on implicitWidth { + Anim { + duration: Appearance.anim.durations.small + } + } + onShouldBeActiveChanged: { if (shouldBeActive) { timer.stop(); @@ -84,12 +90,13 @@ Item { id: content active: false - anchors.horizontalCenter: parent.horizontalCenter + anchors.left: parent.left anchors.top: parent.top visible: false sourceComponent: Content { panels: root.panels + screen: root.screen visibilities: root.visibilities Component.onCompleted: root.contentHeight = implicitHeight -- 2.47.3 From 8f427d06d01c47d04f109a49df294501b3a17e27 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Fri, 13 Mar 2026 16:27:31 +0100 Subject: [PATCH 25/47] dock --- Modules/Dock/Content.qml | 104 ++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 41 deletions(-) diff --git a/Modules/Dock/Content.qml b/Modules/Dock/Content.qml index 955d955..47b1d13 100644 --- a/Modules/Dock/Content.qml +++ b/Modules/Dock/Content.qml @@ -21,8 +21,10 @@ Item { property real dragY: 0 property string draggedAppId: "" property var draggedModelData: null + property bool dropAnimating: false readonly property int padding: Appearance.padding.small required property var panels + property var pendingCommitIds: [] readonly property int rounding: Appearance.rounding.large required property ShellScreen screen required property PersistentProperties visibilities @@ -41,15 +43,45 @@ Item { root.dragX = pos.x; root.dragY = pos.y; root.dragActive = true; + root.dropAnimating = false; + root.pendingCommitIds = []; } function endVisualDrag() { const ids = root.visualIds.slice(); + const finalIndex = root.visualIds.indexOf(root.draggedAppId); + const finalItem = dockRow.itemAtIndex(finalIndex); + + // Stop sending drag events now, but keep the proxy alive while it settles. + root.dragActive = false; + + // In a dock, the destination delegate should normally be instantiated. + // If not, just finish immediately. + if (!finalItem) { + root.pendingCommitIds = ids; + root.finishVisualDrag(); + return; + } + + const pos = finalItem.mapToItem(root, 0, 0); + + root.pendingCommitIds = ids; + root.dropAnimating = true; + + settleX.to = pos.x; + settleY.to = pos.y; + settleAnim.start(); + } + + function finishVisualDrag() { + const ids = root.pendingCommitIds.slice(); root.dragActive = false; + root.dropAnimating = false; root.draggedAppId = ""; root.draggedModelData = null; root.visualIds = []; + root.pendingCommitIds = []; TaskbarApps.commitVisualOrder(ids); } @@ -77,7 +109,29 @@ Item { } implicitHeight: Config.dock.height + root.padding * 2 - implicitWidth: root.dockContentWidth + root.padding * 2 + implicitWidth: dockRow.contentWidth + root.padding * 2 + + ParallelAnimation { + id: settleAnim + + onFinished: root.finishVisualDrag() + + Anim { + id: settleX + + duration: Appearance.anim.durations.normal + property: "dragX" + target: root + } + + Anim { + id: settleY + + duration: Appearance.anim.durations.normal + property: "dragY" + target: root + } + } Component { id: dockDelegate @@ -88,7 +142,6 @@ Item { readonly property string appId: modelData.appId readonly property bool isSeparator: appId === TaskbarApps.separatorId required property var modelData - property bool removing: false function previewReorder(drag) { const source = drag.source; @@ -104,31 +157,20 @@ Item { root.previewVisualMove(from, hovered, drag.x < width / 2); } - function startDetachedRemove() { - const p = mapToItem(removalLayer, 0, 0); - - removing = true; - ListView.delayRemove = true; - - parent = removalLayer; - x = p.x; - y = p.y; - - removeAnim.start(); - } - height: Config.dock.height - transformOrigin: Item.Center width: isSeparator ? 1 : Config.dock.height - z: removing ? 1 : 0 - ListView.onRemove: startDetachedRemove() + ListView.onRemove: removeAnim.start() onEntered: drag => previewReorder(drag) onPositionChanged: drag => previewReorder(drag) SequentialAnimation { id: removeAnim + ScriptAction { + script: slot.ListView.delayRemove = true + } + ParallelAnimation { Anim { property: "opacity" @@ -163,7 +205,7 @@ Item { DragHandler { id: dragHandler - enabled: !slot.isSeparator && !slot.removing + enabled: !slot.isSeparator grabPermissions: PointerHandler.CanTakeOverFromAnything target: null xAxis.enabled: true @@ -210,7 +252,7 @@ Item { boundsBehavior: Flickable.StopAtBounds height: Config.dock.height implicitWidth: root.dockContentWidth + Config.dock.height - interactive: !root.dragActive + interactive: !(root.dragActive || root.dropAnimating) model: visualModel orientation: ListView.Horizontal spacing: Appearance.padding.smaller @@ -244,32 +286,12 @@ Item { properties: "x,y" } } - remove: Transition { - ParallelAnimation { - Anim { - property: "opacity" - to: 0 - } - - Anim { - property: "scale" - to: 0.5 - } - } - } Component.onCompleted: { Qt.callLater(() => enableAddAnimation = true); } } - Item { - id: removalLayer - - anchors.fill: parent - z: 9998 - } - Item { id: dragProxy @@ -281,7 +303,7 @@ Item { Drag.hotSpot.y: height / 2 Drag.source: dragProxy height: root.dragHeight - visible: root.dragActive && !!root.draggedModelData + visible: (root.dragActive || root.dropAnimating) && !!root.draggedModelData width: root.dragWidth x: root.dragX y: root.dragY -- 2.47.3 From 31df6a6cdf63064dfafde6f85a42f98fa7efa4f8 Mon Sep 17 00:00:00 2001 From: inorishio Date: Fri, 13 Mar 2026 16:28:03 +0100 Subject: [PATCH 26/47] settings --- Components/CustomSplitButtonRow.qml | 14 +- Modules/Settings/Categories.qml | 2 +- Modules/Settings/Categories/Appearance.qml | 64 +++++++- Modules/Settings/Content.qml | 2 +- Modules/Settings/Controls/Separator.qml | 12 ++ Modules/Settings/Controls/SettingInput.qml | 65 ++++++++ Modules/Settings/Controls/SettingList.qml | 155 ++++++++++++++++++++ Modules/Settings/Controls/SettingSwitch.qml | 41 ++++-- Modules/Settings/Controls/VSeparator.qml | 12 ++ 9 files changed, 335 insertions(+), 32 deletions(-) create mode 100644 Modules/Settings/Controls/Separator.qml create mode 100644 Modules/Settings/Controls/SettingInput.qml create mode 100644 Modules/Settings/Controls/SettingList.qml create mode 100644 Modules/Settings/Controls/VSeparator.qml diff --git a/Components/CustomSplitButtonRow.qml b/Components/CustomSplitButtonRow.qml index 763ca8a..5b4fe79 100644 --- a/Components/CustomSplitButtonRow.qml +++ b/Components/CustomSplitButtonRow.qml @@ -4,7 +4,7 @@ import QtQuick import QtQuick.Layouts import qs.Config -CustomRect { +Item { id: root property alias active: splitButton.active @@ -18,23 +18,23 @@ CustomRect { signal selected(item: MenuItem) Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 clip: false - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) - implicitHeight: row.implicitHeight + Appearance.padding.large * 2 - opacity: enabled ? 1.0 : 0.5 - radius: Appearance.rounding.normal z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1 RowLayout { id: row - anchors.fill: parent - anchors.margins: Appearance.padding.large + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter spacing: Appearance.spacing.normal CustomText { Layout.fillWidth: true color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.larger text: root.label } diff --git a/Modules/Settings/Categories.qml b/Modules/Settings/Categories.qml index 08bcacc..7768d11 100644 --- a/Modules/Settings/Categories.qml +++ b/Modules/Settings/Categories.qml @@ -148,7 +148,7 @@ Item { Layout.fillHeight: true Layout.preferredWidth: icon.contentWidth color: categoryItem.index === clayout.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - font.pointSize: 22 + font.pointSize: Appearance.font.size.small * 2 text: categoryItem.icon verticalAlignment: Text.AlignVCenter } diff --git a/Modules/Settings/Categories/Appearance.qml b/Modules/Settings/Categories/Appearance.qml index 78c18a0..613ba81 100644 --- a/Modules/Settings/Categories/Appearance.qml +++ b/Modules/Settings/Categories/Appearance.qml @@ -19,7 +19,7 @@ CustomRect { CustomRect { Layout.fillWidth: true - Layout.preferredHeight: colorLayout.implicitHeight + Layout.preferredHeight: colorLayout.implicitHeight + Appearance.padding.normal * 2 color: DynamicColors.tPalette.m3surfaceContainer ColumnLayout { @@ -28,15 +28,46 @@ CustomRect { anchors.left: parent.left anchors.margins: Appearance.padding.large anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter Settings { - name: "smth" + name: "Color" } SettingSwitch { - name: "wallust" + name: "Automatic color scheme" object: Config.general.color - setting: "wallust" + setting: "schemeGeneration" + } + + Separator { + } + + SettingSwitch { + name: "Smart color scheme" + object: Config.general.color + setting: "smart" + } + + Separator { + } + + SettingInput { + name: "Schedule dark mode start" + object: Config.general.color + setting: "scheduleDarkStart" + } + + Separator { + } + + SettingInput { + name: "Schedule dark mode end" + object: Config.general.color + setting: "scheduleDarkEnd" + } + + Separator { } CustomSplitButtonRow { @@ -71,6 +102,25 @@ CustomRect { } } } + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: idleLayout.implicitHeight + Appearance.padding.normal * 2 + color: DynamicColors.tPalette.m3surfaceContainer + + ColumnLayout { + id: idleLayout + + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Settings { + name: "Idle" + } + } + } } component Settings: CustomRect { @@ -78,7 +128,7 @@ CustomRect { required property string name - Layout.preferredHeight: 42 + Layout.preferredHeight: 60 Layout.preferredWidth: 200 radius: 4 @@ -86,10 +136,10 @@ CustomRect { id: text anchors.left: parent.left - anchors.margins: Appearance.padding.smaller anchors.right: parent.right + anchors.top: parent.top font.bold: true - font.pointSize: 32 + font.pointSize: Appearance.font.size.large * 2 text: settingsItem.name verticalAlignment: Text.AlignVCenter } diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml index 1037a21..e6543a1 100644 --- a/Modules/Settings/Content.qml +++ b/Modules/Settings/Content.qml @@ -13,7 +13,7 @@ Item { property string currentCategory: "general" readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 - readonly property real nonAnimWidth: view.implicitWidth + 500 + viewWrapper.anchors.margins * 2 + readonly property real nonAnimWidth: view.implicitWidth + 700 + viewWrapper.anchors.margins * 2 required property PersistentProperties visibilities implicitHeight: nonAnimHeight diff --git a/Modules/Settings/Controls/Separator.qml b/Modules/Settings/Controls/Separator.qml new file mode 100644 index 0000000..86fcab4 --- /dev/null +++ b/Modules/Settings/Controls/Separator.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + id: root + + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: DynamicColors.tPalette.m3outlineVariant +} diff --git a/Modules/Settings/Controls/SettingInput.qml b/Modules/Settings/Controls/SettingInput.qml new file mode 100644 index 0000000..58ffe24 --- /dev/null +++ b/Modules/Settings/Controls/SettingInput.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property var object + required property string setting + + function formattedValue(): string { + const value = root.object[root.setting]; + + console.log(value); + if (value === null || value === undefined) + return ""; + + return String(value); + } + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + CustomText { + id: text + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomRect { + id: rect + + Layout.preferredHeight: 33 + Layout.preferredWidth: Math.max(Math.min(textField.contentWidth + Appearance.padding.normal * 3, 200), 50) + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + + CustomTextField { + id: textField + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: root.formattedValue() + + onEditingFinished: { + root.object[root.setting] = textField.text; + Config.save(); + } + } + } + } +} diff --git a/Modules/Settings/Controls/SettingList.qml b/Modules/Settings/Controls/SettingList.qml new file mode 100644 index 0000000..daec7b0 --- /dev/null +++ b/Modules/Settings/Controls/SettingList.qml @@ -0,0 +1,155 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property list modelData + required property string name + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + CustomText { + id: text + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + VSeparator { + } + + ColumnLayout { + id: cLayout + + RowLayout { + id: timeLayout + + Layout.fillWidth: true + + CustomText { + id: timeText + + Layout.fillWidth: true + text: root.modelData.name + } + + CustomRect { + id: timeRect + + Layout.preferredHeight: 33 + Layout.preferredWidth: Math.max(Math.min(timeField.contentWidth + Appearance.padding.normal * 3, 200), 50) + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + + CustomTextField { + id: timeField + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: root.modelData.timeout + + onEditingFinished: { + root.modelData.timeout = timeField.text; + Config.save(); + } + } + } + } + + Separator { + } + + RowLayout { + id: idleLayout + + Layout.fillWidth: true + + CustomText { + id: idleText + + Layout.fillWidth: true + text: root.modelData.name + } + + CustomRect { + id: idleRect + + Layout.preferredHeight: 33 + Layout.preferredWidth: Math.max(Math.min(idleField.contentWidth + Appearance.padding.normal * 3, 200), 50) + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + + CustomTextField { + id: idleField + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: root.modelData.idleAction + + onEditingFinished: { + root.modelData.idleAction = idleField.text; + Config.save(); + } + } + } + } + + Separator { + } + + Loader { + id: loader + + Layout.fillWidth: true + active: root.modelData.activeAction ?? false + + RowLayout { + id: actionLayout + + CustomText { + id: actionText + + Layout.fillWidth: true + text: root.modelData.name + } + + CustomRect { + id: actionRect + + Layout.preferredHeight: 33 + Layout.preferredWidth: Math.max(Math.min(actionField.contentWidth + Appearance.padding.normal * 3, 200), 50) + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + + CustomTextField { + id: actionField + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: root.modelData.activeAction + + onEditingFinished: { + root.modelData.activeAction = actionField.text; + Config.save(); + } + } + } + } + } + } + } +} diff --git a/Modules/Settings/Controls/SettingSwitch.qml b/Modules/Settings/Controls/SettingSwitch.qml index 524e740..09cff93 100644 --- a/Modules/Settings/Controls/SettingSwitch.qml +++ b/Modules/Settings/Controls/SettingSwitch.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import qs.Components import qs.Config -RowLayout { +Item { id: root required property string name @@ -11,26 +11,35 @@ RowLayout { required property string setting Layout.fillWidth: true - Layout.preferredHeight: 42 + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 - CustomText { - id: text + RowLayout { + id: row - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - font.pointSize: 16 - text: root.name - } + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter - CustomSwitch { - id: cswitch + CustomText { + id: text - Layout.alignment: Qt.AlignRight - checked: root.object[root.setting] + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } - onToggled: { - root.object[root.setting] = checked; - Config.save(); + CustomSwitch { + id: cswitch + + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + checked: root.object[root.setting] + + onToggled: { + root.object[root.setting] = checked; + Config.save(); + } } } } diff --git a/Modules/Settings/Controls/VSeparator.qml b/Modules/Settings/Controls/VSeparator.qml new file mode 100644 index 0000000..c7d507e --- /dev/null +++ b/Modules/Settings/Controls/VSeparator.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + id: root + + Layout.fillHeight: true + Layout.preferredWidth: 1 + color: DynamicColors.tPalette.m3outlineVariant +} -- 2.47.3 From b7ca2f5c934cf342b79926edca5c9088a156da73 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Fri, 13 Mar 2026 17:35:46 +0100 Subject: [PATCH 27/47] dock --- Components/CustomButton.qml | 5 +- Config/General.qml | 2 + Drawers/Panels.qml | 1 + Modules/Settings/Categories/Appearance.qml | 11 +-- .../Settings/Categories/Appearance/Idle.qml | 57 ++++++++++++ Modules/Settings/Content.qml | 5 +- Modules/Settings/Controls/SettingList.qml | 93 +++++++++++-------- Modules/Settings/Wrapper.qml | 22 +++-- 8 files changed, 139 insertions(+), 57 deletions(-) create mode 100644 Modules/Settings/Categories/Appearance/Idle.qml diff --git a/Components/CustomButton.qml b/Components/CustomButton.qml index 79580f7..adc521e 100644 --- a/Components/CustomButton.qml +++ b/Components/CustomButton.qml @@ -1,12 +1,13 @@ import QtQuick import QtQuick.Controls +import qs.Config Button { id: control - required property color bgColor + property color bgColor: DynamicColors.palette.m3primary property int radius: 4 - required property color textColor + property color textColor: DynamicColors.palette.m3onPrimary background: CustomRect { color: control.bgColor diff --git a/Config/General.qml b/Config/General.qml index aac4cb1..ce9d5f9 100644 --- a/Config/General.qml +++ b/Config/General.qml @@ -30,10 +30,12 @@ JsonObject { component Idle: JsonObject { property list timeouts: [ { + name: "Lock", timeout: 180, idleAction: "lock" }, { + name: "Screen", timeout: 300, idleAction: "dpms off", activeAction: "dpms on" diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 4aa5000..fd79941 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -143,6 +143,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top panels: root + screen: root.screen visibilities: root.visibilities } diff --git a/Modules/Settings/Categories/Appearance.qml b/Modules/Settings/Categories/Appearance.qml index 613ba81..9d002d3 100644 --- a/Modules/Settings/Categories/Appearance.qml +++ b/Modules/Settings/Categories/Appearance.qml @@ -5,12 +5,15 @@ import QtQuick.Layouts import qs.Components import qs.Modules as Modules import qs.Modules.Settings.Controls +import qs.Modules.Settings.Categories.Appearance import qs.Config import qs.Helpers -CustomRect { +CustomFlickable { id: root + contentHeight: clayout.implicitHeight + ColumnLayout { id: clayout @@ -108,17 +111,13 @@ CustomRect { Layout.preferredHeight: idleLayout.implicitHeight + Appearance.padding.normal * 2 color: DynamicColors.tPalette.m3surfaceContainer - ColumnLayout { + Idle { id: idleLayout anchors.left: parent.left anchors.margins: Appearance.padding.large anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - - Settings { - name: "Idle" - } } } } diff --git a/Modules/Settings/Categories/Appearance/Idle.qml b/Modules/Settings/Categories/Appearance/Idle.qml new file mode 100644 index 0000000..f3c6f40 --- /dev/null +++ b/Modules/Settings/Categories/Appearance/Idle.qml @@ -0,0 +1,57 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config +import qs.Modules.Settings.Controls + +ColumnLayout { + id: root + + function addTimeoutEntry() { + let list = [...Config.general.idle.timeouts]; + + list.push({ + name: "New Entry", + timeout: 600, + idleAction: "lock" + }); + + Config.general.idle.timeouts = list; + Config.save(); + } + + function updateTimeoutEntry(i, key, value) { + const list = [...Config.general.idle.timeouts]; + let entry = list[i]; + + entry[key] = value; + list[i] = entry; + + Config.general.idle.timeouts = list; + Config.save(); + } + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + Repeater { + model: Config.general.idle.timeouts + + SettingList { + Layout.fillWidth: true + + onAddActiveActionRequested: { + root.updateTimeoutEntry(index, "activeAction", ""); + } + onFieldEdited: function (key, value) { + root.updateTimeoutEntry(index, key, value); + } + } + } + + CustomButton { + text: qsTr("Add timeout entry") + + onClicked: root.addTimeoutEntry() + } +} diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml index e6543a1..24a40b8 100644 --- a/Modules/Settings/Content.qml +++ b/Modules/Settings/Content.qml @@ -12,8 +12,9 @@ Item { id: root property string currentCategory: "general" - readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 - readonly property real nonAnimWidth: view.implicitWidth + 700 + viewWrapper.anchors.margins * 2 + readonly property real nonAnimHeight: (screen.height - 400) + viewWrapper.anchors.margins * 2 + readonly property real nonAnimWidth: view.implicitWidth + (screen.width - 400 * 2) + viewWrapper.anchors.margins * 2 + required property ShellScreen screen required property PersistentProperties visibilities implicitHeight: nonAnimHeight diff --git a/Modules/Settings/Controls/SettingList.qml b/Modules/Settings/Controls/SettingList.qml index daec7b0..0321761 100644 --- a/Modules/Settings/Controls/SettingList.qml +++ b/Modules/Settings/Controls/SettingList.qml @@ -6,12 +6,25 @@ import qs.Config Item { id: root - required property list modelData - required property string name + required property int index + required property var modelData + + signal addActiveActionRequested + signal fieldEdited(string key, var value) Layout.fillWidth: true Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: -(Appearance.spacing.smaller / 2) + color: DynamicColors.tPalette.m3outlineVariant + implicitHeight: 1 + visible: root.index !== 0 + } + RowLayout { id: row @@ -19,14 +32,13 @@ Item { anchors.margins: Appearance.padding.small anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.large CustomText { - id: text - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true + Layout.preferredWidth: root.width / 2 font.pointSize: Appearance.font.size.larger - text: root.name + text: root.modelData.name } VSeparator { @@ -35,21 +47,17 @@ Item { ColumnLayout { id: cLayout - RowLayout { - id: timeLayout + Layout.fillWidth: true + RowLayout { Layout.fillWidth: true CustomText { - id: timeText - Layout.fillWidth: true - text: root.modelData.name + text: qsTr("Timeout") } CustomRect { - id: timeRect - Layout.preferredHeight: 33 Layout.preferredWidth: Math.max(Math.min(timeField.contentWidth + Appearance.padding.normal * 3, 200), 50) color: DynamicColors.tPalette.m3surface @@ -60,11 +68,10 @@ Item { anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter - text: root.modelData.timeout + text: String(root.modelData.timeout ?? "") onEditingFinished: { - root.modelData.timeout = timeField.text; - Config.save(); + root.fieldEdited("timeout", Number(text)); } } } @@ -74,20 +81,14 @@ Item { } RowLayout { - id: idleLayout - Layout.fillWidth: true CustomText { - id: idleText - Layout.fillWidth: true - text: root.modelData.name + text: qsTr("Idle Action") } CustomRect { - id: idleRect - Layout.preferredHeight: 33 Layout.preferredWidth: Math.max(Math.min(idleField.contentWidth + Appearance.padding.normal * 3, 200), 50) color: DynamicColors.tPalette.m3surface @@ -98,11 +99,10 @@ Item { anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter - text: root.modelData.idleAction + text: root.modelData.idleAction ?? "" onEditingFinished: { - root.modelData.idleAction = idleField.text; - Config.save(); + root.fieldEdited("idleAction", text); } } } @@ -111,25 +111,23 @@ Item { Separator { } - Loader { - id: loader - + Item { Layout.fillWidth: true - active: root.modelData.activeAction ?? false + implicitHeight: activeActionRow.visible ? activeActionRow.implicitHeight : addButtonRow.implicitHeight RowLayout { - id: actionLayout + id: activeActionRow + + anchors.left: parent.left + anchors.right: parent.right + visible: root.modelData.activeAction !== undefined CustomText { - id: actionText - Layout.fillWidth: true - text: root.modelData.name + text: qsTr("Active Action") } CustomRect { - id: actionRect - Layout.preferredHeight: 33 Layout.preferredWidth: Math.max(Math.min(actionField.contentWidth + Appearance.padding.normal * 3, 200), 50) color: DynamicColors.tPalette.m3surface @@ -140,15 +138,32 @@ Item { anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter - text: root.modelData.activeAction + text: root.modelData.activeAction ?? "" onEditingFinished: { - root.modelData.activeAction = actionField.text; - Config.save(); + root.fieldEdited("activeAction", text); } } } } + + RowLayout { + id: addButtonRow + + anchors.left: parent.left + anchors.right: parent.right + visible: root.modelData.activeAction === undefined + + Item { + Layout.fillWidth: true + } + + CustomButton { + text: qsTr("Add active action") + + onClicked: root.addActiveActionRequested() + } + } } } } diff --git a/Modules/Settings/Wrapper.qml b/Modules/Settings/Wrapper.qml index dec6264..dbcd141 100644 --- a/Modules/Settings/Wrapper.qml +++ b/Modules/Settings/Wrapper.qml @@ -8,6 +8,7 @@ Item { id: root required property var panels + required property ShellScreen screen required property PersistentProperties visibilities implicitHeight: 0 @@ -46,16 +47,21 @@ Item { } ] - Loader { - id: content + CustomClippingRect { + anchors.fill: parent - active: true - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - visible: true + Loader { + id: content - sourceComponent: Content { - visibilities: root.visibilities + active: true + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + visible: true + + sourceComponent: Content { + screen: root.screen + visibilities: root.visibilities + } } } } -- 2.47.3 From 8bc7826f2632f5d73d9291824ab642d8844fd6af Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Fri, 13 Mar 2026 20:07:03 +0100 Subject: [PATCH 28/47] dock --- Modules/Settings/Background.qml | 2 +- Modules/Settings/Categories.qml | 4 +- Modules/Settings/Categories/Appearance.qml | 7 +- .../Settings/Categories/Appearance/Idle.qml | 9 +- Modules/Settings/Content.qml | 10 +-- Modules/Settings/Controls/SettingList.qml | 83 ++++++++++++++++++- 6 files changed, 97 insertions(+), 18 deletions(-) diff --git a/Modules/Settings/Background.qml b/Modules/Settings/Background.qml index cc242f7..3004b3d 100644 --- a/Modules/Settings/Background.qml +++ b/Modules/Settings/Background.qml @@ -7,7 +7,7 @@ ShapePath { id: root readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real rounding: 8 + readonly property real rounding: Appearance.rounding.large readonly property real roundingY: flatten ? wrapper.height / 2 : rounding required property Wrapper wrapper diff --git a/Modules/Settings/Categories.qml b/Modules/Settings/Categories.qml index 7768d11..c4401bc 100644 --- a/Modules/Settings/Categories.qml +++ b/Modules/Settings/Categories.qml @@ -86,10 +86,10 @@ Item { } } - CustomRect { + CustomClippingRect { anchors.fill: parent color: DynamicColors.tPalette.m3surfaceContainer - radius: 4 + radius: Appearance.rounding.normal CustomListView { id: clayout diff --git a/Modules/Settings/Categories/Appearance.qml b/Modules/Settings/Categories/Appearance.qml index 9d002d3..b7979ad 100644 --- a/Modules/Settings/Categories/Appearance.qml +++ b/Modules/Settings/Categories/Appearance.qml @@ -20,10 +20,11 @@ CustomFlickable { anchors.left: parent.left anchors.right: parent.right - CustomRect { + CustomClippingRect { Layout.fillWidth: true Layout.preferredHeight: colorLayout.implicitHeight + Appearance.padding.normal * 2 color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal - Appearance.padding.smaller ColumnLayout { id: colorLayout @@ -106,10 +107,11 @@ CustomFlickable { } } - CustomRect { + CustomClippingRect { Layout.fillWidth: true Layout.preferredHeight: idleLayout.implicitHeight + Appearance.padding.normal * 2 color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal - Appearance.padding.smaller Idle { id: idleLayout @@ -129,7 +131,6 @@ CustomFlickable { Layout.preferredHeight: 60 Layout.preferredWidth: 200 - radius: 4 CustomText { id: text diff --git a/Modules/Settings/Categories/Appearance/Idle.qml b/Modules/Settings/Categories/Appearance/Idle.qml index f3c6f40..f405c1d 100644 --- a/Modules/Settings/Categories/Appearance/Idle.qml +++ b/Modules/Settings/Categories/Appearance/Idle.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Layouts import qs.Components @@ -35,7 +37,7 @@ ColumnLayout { spacing: Appearance.spacing.smaller Repeater { - model: Config.general.idle.timeouts + model: [...Config.general.idle.timeouts] SettingList { Layout.fillWidth: true @@ -49,8 +51,9 @@ ColumnLayout { } } - CustomButton { - text: qsTr("Add timeout entry") + IconButton { + font.pointSize: Appearance.font.size.large + icon: "add" onClicked: root.addTimeoutEntry() } diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml index 24a40b8..de1c8e7 100644 --- a/Modules/Settings/Content.qml +++ b/Modules/Settings/Content.qml @@ -12,8 +12,8 @@ Item { id: root property string currentCategory: "general" - readonly property real nonAnimHeight: (screen.height - 400) + viewWrapper.anchors.margins * 2 - readonly property real nonAnimWidth: view.implicitWidth + (screen.width - 400 * 2) + viewWrapper.anchors.margins * 2 + readonly property real nonAnimHeight: Math.floor(screen.height / 1.5) + viewWrapper.anchors.margins * 2 + readonly property real nonAnimWidth: view.implicitWidth + Math.floor(screen.width / 2) + viewWrapper.anchors.margins * 2 required property ShellScreen screen required property PersistentProperties visibilities @@ -35,12 +35,12 @@ Item { target: root } - ClippingRectangle { + CustomClippingRect { id: viewWrapper anchors.fill: parent anchors.margins: Appearance.padding.smaller - color: "transparent" + radius: Appearance.rounding.large - Appearance.padding.smaller Item { id: view @@ -68,7 +68,7 @@ Item { anchors.right: parent.right anchors.top: parent.top color: DynamicColors.tPalette.m3surfaceContainer - radius: 4 + radius: Appearance.rounding.normal StackView { id: stack diff --git a/Modules/Settings/Controls/SettingList.qml b/Modules/Settings/Controls/SettingList.qml index 0321761..6fcb997 100644 --- a/Modules/Settings/Controls/SettingList.qml +++ b/Modules/Settings/Controls/SettingList.qml @@ -34,11 +34,86 @@ Item { anchors.verticalCenter: parent.verticalCenter spacing: Appearance.spacing.large - CustomText { - Layout.alignment: Qt.AlignLeft + Item { + id: nameCell + + property string draftName: "" + property bool editing: false + + function beginEdit() { + draftName = root.modelData.name ?? ""; + editing = true; + nameEditor.forceActiveFocus(); + } + + function cancelEdit() { + draftName = root.modelData.name ?? ""; + editing = false; + } + + function commitEdit() { + editing = false; + + if (draftName !== (root.modelData.name ?? "")) { + root.fieldEdited("name", draftName); + } + } + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillHeight: true Layout.preferredWidth: root.width / 2 - font.pointSize: Appearance.font.size.larger - text: root.modelData.name + + HoverHandler { + id: nameHover + + } + + CustomText { + anchors.left: parent.left + anchors.right: editButton.left + anchors.rightMargin: Appearance.spacing.small + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight // enable if CustomText supports it + font.pointSize: Appearance.font.size.larger + text: root.modelData.name + visible: !nameCell.editing + } + + IconButton { + id: editButton + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + font.pointSize: Appearance.font.size.large + icon: "edit" + visible: nameHover.hovered && !nameCell.editing + + onClicked: nameCell.beginEdit() + } + + CustomRect { + anchors.fill: parent + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + visible: nameCell.editing + + CustomTextField { + id: nameEditor + + anchors.fill: parent + text: nameCell.draftName + + Keys.onEscapePressed: { + nameCell.cancelEdit(); + } + onEditingFinished: { + nameCell.commitEdit(); + } + onTextEdited: { + nameCell.draftName = nameEditor.text; + } + } + } } VSeparator { -- 2.47.3 From f7b72607802196d70e62349ebc3afe05f15dfbe1 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Sat, 14 Mar 2026 17:29:24 +0100 Subject: [PATCH 29/47] settings --- Components/CustomSplitButton.qml | 24 ++- Components/CustomSplitButtonRow.qml | 3 +- Components/PathMenu.qml | 76 ++++++++ Components/PathViewMenu.qml | 178 ++++++++++++++++++ Helpers/SettingsDropdowns.qml | 60 ++++++ Modules/Settings/Categories.qml | 12 +- Modules/Settings/Categories/Appearance.qml | 60 +++--- Modules/Settings/Categories/General.qml | 36 +--- Modules/Settings/Categories/Lockscreen.qml | 77 ++++++++ .../{Appearance => Lockscreen}/Idle.qml | 23 +++ Modules/Settings/Content.qml | 17 +- Modules/Settings/Controls/SettingList.qml | 16 +- Modules/Settings/Controls/SettingSpinner.qml | 88 +++++++++ Modules/Settings/Controls/SpinnerButton.qml | 42 +++++ 14 files changed, 635 insertions(+), 77 deletions(-) create mode 100644 Components/PathMenu.qml create mode 100644 Components/PathViewMenu.qml create mode 100644 Helpers/SettingsDropdowns.qml create mode 100644 Modules/Settings/Categories/Lockscreen.qml rename Modules/Settings/Categories/{Appearance => Lockscreen}/Idle.qml (74%) create mode 100644 Modules/Settings/Controls/SettingSpinner.qml create mode 100644 Modules/Settings/Controls/SpinnerButton.qml diff --git a/Components/CustomSplitButton.qml b/Components/CustomSplitButton.qml index becabf5..6f9ad95 100644 --- a/Components/CustomSplitButton.qml +++ b/Components/CustomSplitButton.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Layouts import qs.Config +import qs.Helpers Row { id: root @@ -29,8 +30,29 @@ Row { property int type: CustomSplitButton.Filled property real verticalPadding: Appearance.padding.smaller + function closeDropdown(): void { + SettingsDropdowns.close(menu); + } + + function openDropdown(): void { + if (root.disabled) + return; + SettingsDropdowns.open(menu, root); + } + + function toggleDropdown(): void { + if (root.disabled) + return; + SettingsDropdowns.toggle(menu, root); + } + spacing: Math.floor(Appearance.spacing.small / 2) + onExpandedChanged: { + if (!expanded) + SettingsDropdowns.forget(menu); + } + CustomRect { bottomRightRadius: Appearance.rounding.small / 2 color: root.disabled ? root.disabledColor : root.color @@ -109,7 +131,7 @@ Row { id: expandStateLayer function onClicked(): void { - root.expanded = !root.expanded; + root.toggleDropdown(); } color: root.textColor diff --git a/Components/CustomSplitButtonRow.qml b/Components/CustomSplitButtonRow.qml index 5b4fe79..a8295e9 100644 --- a/Components/CustomSplitButtonRow.qml +++ b/Components/CustomSplitButtonRow.qml @@ -47,9 +47,10 @@ Item { menu.onItemSelected: item => { root.selected(item); + splitButton.closeDropdown(); } stateLayer.onClicked: { - splitButton.expanded = !splitButton.expanded; + splitButton.toggleDropdown(); } } } diff --git a/Components/PathMenu.qml b/Components/PathMenu.qml new file mode 100644 index 0000000..0e2d10d --- /dev/null +++ b/Components/PathMenu.qml @@ -0,0 +1,76 @@ +import QtQuick + +Path { + id: root + + required property real viewHeight + required property real viewWidth + + startX: root.viewWidth / 2 + startY: 0 + + PathAttribute { + name: "itemOpacity" + value: 0.25 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (1 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.45 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (2 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.70 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (3 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 1.00 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (4 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.70 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (5 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.45 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight + } + + PathAttribute { + name: "itemOpacity" + value: 0.25 + } +} diff --git a/Components/PathViewMenu.qml b/Components/PathViewMenu.qml new file mode 100644 index 0000000..898dd4b --- /dev/null +++ b/Components/PathViewMenu.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Effects +import qs.Config + +Elevation { + id: root + + required property int currentIndex + property bool expanded + required property int from + property color insideTextColor: DynamicColors.palette.m3onPrimary + property int itemHeight + property int listHeight: 200 + property color outsideTextColor: DynamicColors.palette.m3onSurfaceVariant + readonly property var spinnerModel: root.range(root.from, root.to) + required property int to + property Item triggerItem + + signal itemSelected(item: int) + + function range(first, last) { + let out = []; + for (let i = first; i <= last; ++i) + out.push(i); + return out; + } + + implicitHeight: root.expanded ? view.implicitHeight : 0 + level: root.expanded ? 2 : 0 + radius: itemHeight / 2 + visible: implicitHeight > 0 + + Behavior on implicitHeight { + Anim { + } + } + + onExpandedChanged: { + if (!root.expanded) + root.itemSelected(view.currentIndex + 1); + } + + Component { + id: spinnerDelegate + + Item { + id: wrapper + + readonly property color delegateTextColor: wrapper.PathView.view ? wrapper.PathView.view.delegateTextColor : "white" + required property var modelData + + height: root.itemHeight + opacity: wrapper.PathView.itemOpacity + visible: wrapper.PathView.onPath + width: wrapper.PathView.view ? wrapper.PathView.view.width : 0 + z: wrapper.PathView.isCurrentItem ? 100 : Math.round(wrapper.PathView.itemScale * 100) + + CustomText { + anchors.centerIn: parent + color: wrapper.delegateTextColor + font.pointSize: Appearance.font.size.large + text: wrapper.modelData + } + } + } + + CustomClippingRect { + anchors.fill: parent + color: DynamicColors.palette.m3surfaceContainer + radius: parent.radius + + // Main visible spinner: normal/outside text color + PathView { + id: view + + property color delegateTextColor: root.outsideTextColor + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + currentIndex: root.currentIndex - 1 + delegate: spinnerDelegate + dragMargin: width + highlightRangeMode: PathView.StrictlyEnforceRange + implicitHeight: root.listHeight + model: root.spinnerModel + pathItemCount: 7 + preferredHighlightBegin: 0.5 + preferredHighlightEnd: 0.5 + snapMode: PathView.SnapToItem + + path: PathMenu { + viewHeight: view.height + viewWidth: view.width + } + } + + // The selection rectangle itself + CustomRect { + id: selectionRect + + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3primary + height: root.itemHeight + radius: root.itemHeight / 2 + width: parent.width + z: 2 + } + + // Hidden source: same PathView, but with the "inside selection" text color + Item { + id: selectedTextSource + + anchors.fill: parent + layer.enabled: true + visible: false + + PathView { + id: selectedTextView + + property color delegateTextColor: root.insideTextColor + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + currentIndex: view.currentIndex + delegate: spinnerDelegate + dragMargin: view.dragMargin + highlightRangeMode: view.highlightRangeMode + implicitHeight: root.listHeight + interactive: false + model: view.model + + // Keep this PathView visually locked to the real one + offset: view.offset + pathItemCount: view.pathItemCount + preferredHighlightBegin: view.preferredHighlightBegin + preferredHighlightEnd: view.preferredHighlightEnd + snapMode: view.snapMode + + path: PathMenu { + viewHeight: selectedTextView.height + viewWidth: selectedTextView.width + } + } + } + + // Mask matching the selection rectangle + Item { + id: selectionMask + + anchors.fill: parent + layer.enabled: true + visible: false + + CustomRect { + color: "white" + height: selectionRect.height + radius: selectionRect.radius + width: selectionRect.width + x: selectionRect.x + y: selectionRect.y + } + } + + // Only show the "inside selection" text where the mask exists + MultiEffect { + anchors.fill: selectedTextSource + maskEnabled: true + maskInverted: false + maskSource: selectionMask + source: selectedTextSource + z: 3 + } + } +} diff --git a/Helpers/SettingsDropdowns.qml b/Helpers/SettingsDropdowns.qml new file mode 100644 index 0000000..4a4ac27 --- /dev/null +++ b/Helpers/SettingsDropdowns.qml @@ -0,0 +1,60 @@ +pragma Singleton +import QtQuick + +QtObject { + id: root + + property Item activeMenu: null + property Item activeTrigger: null + + function close(menu) { + if (!menu) + return; + + if (activeMenu === menu) { + activeMenu = null; + activeTrigger = null; + } + + menu.expanded = false; + } + + function closeActive() { + if (activeMenu) + activeMenu.expanded = false; + + activeMenu = null; + activeTrigger = null; + } + + function forget(menu) { + if (activeMenu === menu) { + activeMenu = null; + activeTrigger = null; + } + } + + function hit(item, scenePos) { + if (!item || !item.visible) + return false; + + const p = item.mapFromItem(null, scenePos.x, scenePos.y); + return item.contains(p); + } + + function open(menu, trigger) { + if (activeMenu && activeMenu !== menu) + activeMenu.expanded = false; + + activeMenu = menu; + activeTrigger = trigger || null; + menu.expanded = true; + } + + function toggle(menu, trigger) { + if (activeMenu === menu && menu.expanded) + close(menu); + else + open(menu, trigger); + } +} diff --git a/Modules/Settings/Categories.qml b/Modules/Settings/Categories.qml index c4401bc..bd4da4e 100644 --- a/Modules/Settings/Categories.qml +++ b/Modules/Settings/Categories.qml @@ -94,11 +94,13 @@ Item { CustomListView { id: clayout - anchors.centerIn: parent - contentHeight: contentItem.childrenRect.height + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: Appearance.padding.smaller + anchors.top: parent.top + boundsBehavior: Flickable.StopAtBounds contentWidth: contentItem.childrenRect.width highlightFollowsCurrentItem: false - implicitHeight: contentItem.childrenRect.height implicitWidth: contentItem.childrenRect.width model: listModel spacing: 5 @@ -109,7 +111,7 @@ Item { color: DynamicColors.palette.m3primary implicitHeight: clayout.currentItem?.implicitHeight ?? 0 implicitWidth: clayout.width - radius: 4 + radius: Appearance.rounding.normal - Appearance.padding.smaller y: clayout.currentItem?.y ?? 0 Behavior on y { @@ -131,7 +133,7 @@ Item { implicitHeight: 42 implicitWidth: 200 - radius: 4 + radius: Appearance.rounding.normal - Appearance.padding.smaller RowLayout { id: layout diff --git a/Modules/Settings/Categories/Appearance.qml b/Modules/Settings/Categories/Appearance.qml index b7979ad..1c6d237 100644 --- a/Modules/Settings/Categories/Appearance.qml +++ b/Modules/Settings/Categories/Appearance.qml @@ -5,7 +5,6 @@ import QtQuick.Layouts import qs.Components import qs.Modules as Modules import qs.Modules.Settings.Controls -import qs.Modules.Settings.Categories.Appearance import qs.Config import qs.Helpers @@ -14,13 +13,33 @@ CustomFlickable { contentHeight: clayout.implicitHeight + TapHandler { + acceptedButtons: Qt.LeftButton + + onTapped: function (eventPoint) { + const menu = SettingsDropdowns.activeMenu; + if (!menu) + return; + + const p = eventPoint.scenePosition; + + if (SettingsDropdowns.hit(SettingsDropdowns.activeTrigger, p)) + return; + + if (SettingsDropdowns.hit(menu, p)) + return; + + SettingsDropdowns.closeActive(); + } + } + ColumnLayout { id: clayout anchors.left: parent.left anchors.right: parent.right - CustomClippingRect { + CustomRect { Layout.fillWidth: true Layout.preferredHeight: colorLayout.implicitHeight + Appearance.padding.normal * 2 color: DynamicColors.tPalette.m3surfaceContainer @@ -33,6 +52,7 @@ CustomFlickable { anchors.margins: Appearance.padding.large anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.normal Settings { name: "Color" @@ -56,19 +76,11 @@ CustomFlickable { Separator { } - SettingInput { - name: "Schedule dark mode start" + SettingSpinner { + name: "Schedule dark mode" object: Config.general.color - setting: "scheduleDarkStart" - } - - Separator { - } - - SettingInput { - name: "Schedule dark mode end" - object: Config.general.color - setting: "scheduleDarkEnd" + settings: ["scheduleDarkStart", "scheduleDarkEnd"] + z: 2 } Separator { @@ -106,22 +118,6 @@ CustomFlickable { } } } - - CustomClippingRect { - Layout.fillWidth: true - Layout.preferredHeight: idleLayout.implicitHeight + Appearance.padding.normal * 2 - color: DynamicColors.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - Appearance.padding.smaller - - Idle { - id: idleLayout - - anchors.left: parent.left - anchors.margins: Appearance.padding.large - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - } - } } component Settings: CustomRect { @@ -135,9 +131,7 @@ CustomFlickable { CustomText { id: text - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + anchors.fill: parent font.bold: true font.pointSize: Appearance.font.size.large * 2 text: settingsItem.name diff --git a/Modules/Settings/Categories/General.qml b/Modules/Settings/Categories/General.qml index 3e8335c..e35999b 100644 --- a/Modules/Settings/Categories/General.qml +++ b/Modules/Settings/Categories/General.qml @@ -14,13 +14,6 @@ CustomRect { id: clayout anchors.fill: parent - - Settings { - name: "apps" - } - - Item { - } } component Settings: CustomRect { @@ -28,28 +21,17 @@ CustomRect { required property string name - implicitHeight: 42 - implicitWidth: 200 - radius: 4 + Layout.preferredHeight: 60 + Layout.preferredWidth: 200 - RowLayout { - id: layout + CustomText { + id: text - anchors.left: parent.left - anchors.margins: Appearance.padding.smaller - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - CustomText { - id: text - - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - Layout.fillHeight: true - Layout.fillWidth: true - Layout.leftMargin: Appearance.spacing.normal - text: settingsItem.name - verticalAlignment: Text.AlignVCenter - } + anchors.fill: parent + font.bold: true + font.pointSize: Appearance.font.size.large * 2 + text: settingsItem.name + verticalAlignment: Text.AlignVCenter } } } diff --git a/Modules/Settings/Categories/Lockscreen.qml b/Modules/Settings/Categories/Lockscreen.qml new file mode 100644 index 0000000..570aecc --- /dev/null +++ b/Modules/Settings/Categories/Lockscreen.qml @@ -0,0 +1,77 @@ +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Modules as Modules +import qs.Modules.Settings.Categories.Lockscreen +import qs.Config +import qs.Helpers + +CustomFlickable { + id: root + + contentHeight: clayout.implicitHeight + + TapHandler { + acceptedButtons: Qt.LeftButton + + onTapped: function (eventPoint) { + const menu = SettingsDropdowns.activeMenu; + if (!menu) + return; + + const p = eventPoint.scenePosition; + + if (SettingsDropdowns.hit(SettingsDropdowns.activeTrigger, p)) + return; + + if (SettingsDropdowns.hit(menu, p)) + return; + + SettingsDropdowns.closeActive(); + } + } + + ColumnLayout { + id: clayout + + anchors.fill: parent + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: idleLayout.implicitHeight + Appearance.padding.normal * 2 + color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal - Appearance.padding.smaller + z: -1 + + Idle { + id: idleLayout + + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + } + } + + component Settings: CustomRect { + id: settingsItem + + required property string name + + Layout.preferredHeight: 60 + Layout.preferredWidth: 200 + + CustomText { + id: text + + anchors.fill: parent + font.bold: true + font.pointSize: Appearance.font.size.large * 2 + text: settingsItem.name + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/Modules/Settings/Categories/Appearance/Idle.qml b/Modules/Settings/Categories/Lockscreen/Idle.qml similarity index 74% rename from Modules/Settings/Categories/Appearance/Idle.qml rename to Modules/Settings/Categories/Lockscreen/Idle.qml index f405c1d..eba0d67 100644 --- a/Modules/Settings/Categories/Appearance/Idle.qml +++ b/Modules/Settings/Categories/Lockscreen/Idle.qml @@ -36,6 +36,10 @@ ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller + Settings { + name: "Idle Monitors" + } + Repeater { model: [...Config.general.idle.timeouts] @@ -57,4 +61,23 @@ ColumnLayout { onClicked: root.addTimeoutEntry() } + + component Settings: CustomRect { + id: settingsItem + + required property string name + + Layout.preferredHeight: 60 + Layout.preferredWidth: 200 + + CustomText { + id: text + + anchors.fill: parent + font.bold: true + font.pointSize: Appearance.font.size.large * 2 + text: settingsItem.name + verticalAlignment: Text.AlignVCenter + } + } } diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml index de1c8e7..3bb23a2 100644 --- a/Modules/Settings/Content.qml +++ b/Modules/Settings/Content.qml @@ -23,13 +23,14 @@ Item { Connections { function onCurrentCategoryChanged() { stack.pop(); - if (currentCategory === "general") { + if (currentCategory === "general") stack.push(general); - } else if (currentCategory === "wallpaper") { + else if (currentCategory === "wallpaper") stack.push(background); - } else if (currentCategory === "appearance") { + else if (currentCategory === "appearance") stack.push(appearance); - } + else if (currentCategory === "lockscreen") + stack.push(lockscreen); } target: root @@ -48,7 +49,6 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left anchors.top: parent.top - implicitHeight: layout.implicitHeight implicitWidth: layout.implicitWidth Categories { @@ -100,4 +100,11 @@ Item { Cat.Appearance { } } + + Component { + id: lockscreen + + Cat.Lockscreen { + } + } } diff --git a/Modules/Settings/Controls/SettingList.qml b/Modules/Settings/Controls/SettingList.qml index 6fcb997..2a905ca 100644 --- a/Modules/Settings/Controls/SettingList.qml +++ b/Modules/Settings/Controls/SettingList.qml @@ -229,14 +229,20 @@ Item { anchors.right: parent.right visible: root.modelData.activeAction === undefined - Item { - Layout.fillWidth: true + IconButton { + id: button + + Layout.alignment: Qt.AlignLeft + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: console.log(button.width) + // onClicked: root.addActiveActionRequested() } - CustomButton { + CustomText { + Layout.alignment: Qt.AlignLeft text: qsTr("Add active action") - - onClicked: root.addActiveActionRequested() } } } diff --git a/Modules/Settings/Controls/SettingSpinner.qml b/Modules/Settings/Controls/SettingSpinner.qml new file mode 100644 index 0000000..960e5ca --- /dev/null +++ b/Modules/Settings/Controls/SettingSpinner.qml @@ -0,0 +1,88 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property var object + required property list settings + + function commitChoice(choice: int, setting: string): void { + root.object[setting] = choice; + Config.save(); + } + + function formattedValue(setting: string): string { + const value = root.object[setting]; + + console.log(value); + if (value === null || value === undefined) + return ""; + + return String(value); + } + + function hourToAmPm(hour) { + var h = Number(hour) % 24; + var d = new Date(2000, 0, 1, h, 0, 0); + return Qt.formatTime(d, "h AP"); + } + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + CustomText { + id: text + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomText { + Layout.alignment: Qt.AlignLeft + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + text: qsTr("Dark mode will turn on at %1, and turn off at %2.").arg(root.hourToAmPm(root.object[root.settings[0]])).arg(root.hourToAmPm(root.object[root.settings[1]])) + } + } + + SpinnerButton { + Layout.preferredHeight: Appearance.font.size.large + Appearance.padding.smaller * 2 + Layout.preferredWidth: height * 2 + currentIndex: root.object[root.settings[0]] + text: root.formattedValue(root.settings[0]) + + menu.onItemSelected: item => { + root.commitChoice(item, root.settings[0]); + } + } + + SpinnerButton { + Layout.preferredHeight: Appearance.font.size.large + Appearance.padding.smaller * 2 + Layout.preferredWidth: height * 2 + currentIndex: root.object[root.settings[1]] + text: root.formattedValue(root.settings[1]) + + menu.onItemSelected: item => { + root.commitChoice(item, root.settings[1]); + } + } + } +} diff --git a/Modules/Settings/Controls/SpinnerButton.qml b/Modules/Settings/Controls/SpinnerButton.qml new file mode 100644 index 0000000..7d5bee2 --- /dev/null +++ b/Modules/Settings/Controls/SpinnerButton.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Helpers +import qs.Config + +CustomRect { + id: root + + property alias currentIndex: menu.currentIndex + property alias expanded: menu.expanded + property alias label: label + property alias menu: menu + property alias text: label.text + + color: DynamicColors.palette.m3primary + radius: Appearance.rounding.full + + CustomText { + id: label + + anchors.centerIn: parent + color: DynamicColors.palette.m3onPrimary + font.pointSize: Appearance.font.size.large + } + + StateLayer { + function onClicked(): void { + SettingsDropdowns.toggle(menu, root); + } + } + + PathViewMenu { + id: menu + + anchors.centerIn: parent + from: 1 + implicitWidth: root.width + itemHeight: root.height + to: 24 + } +} -- 2.47.3 From 9c955581faa9c967003a2d8efe2bac39889adefd Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Sun, 15 Mar 2026 18:50:26 +0100 Subject: [PATCH 30/47] drawing clear on right click --- Drawers/DrawingInput.qml | 7 ++++++- Drawers/Windows.qml | 1 + Modules/Notifications/Wrapper.qml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Drawers/DrawingInput.qml b/Drawers/DrawingInput.qml index 51a785f..c8f2c18 100644 --- a/Drawers/DrawingInput.qml +++ b/Drawers/DrawingInput.qml @@ -1,6 +1,7 @@ import Quickshell import QtQuick import qs.Components +import qs.Config CustomMouseArea { id: root @@ -12,7 +13,7 @@ CustomMouseArea { required property PersistentProperties visibilities function inLeftPanel(panel: Item, x: real, y: real): bool { - return x < panel.x + panel.width && withinPanelHeight(panel, x, y); + return x < panel.x + panel.width + Config.barConfig.border && withinPanelHeight(panel, x, y); } function withinPanelHeight(panel: Item, x: real, y: real): bool { @@ -20,6 +21,7 @@ CustomMouseArea { return y >= panelY && y <= panelY + panel.height; } + acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: root.visibilities.isDrawing ? parent : undefined hoverEnabled: true visible: root.visibilities.isDrawing @@ -45,6 +47,9 @@ CustomMouseArea { root.drawing.beginStroke(x, y); return; } + + if (event.buttons & Qt.RightButton) + root.drawing.clear(); } onReleased: { if (root.visibilities.isDrawing) diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index e38a84a..a7ec012 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -153,6 +153,7 @@ Variants { bar: bar panels: panels visibilities: visibilities + z: 1 } } diff --git a/Modules/Notifications/Wrapper.qml b/Modules/Notifications/Wrapper.qml index fda5789..1e7038f 100644 --- a/Modules/Notifications/Wrapper.qml +++ b/Modules/Notifications/Wrapper.qml @@ -14,7 +14,7 @@ Item { states: State { name: "hidden" - when: root.visibilities.sidebar + when: root.visibilities.sidebar || root.visibilities.dashboard || (root.panels.popouts.hasCurrent && root.panels.popouts.currentName.startsWith("traymenu")) PropertyChanges { root.implicitHeight: 0 -- 2.47.3 From a4e086192d27d277039042e2b2f00a31316c5a2a Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Sun, 15 Mar 2026 22:41:10 +0100 Subject: [PATCH 31/47] drawing sliders --- Components/BaseStyledSlider.qml | 165 ++++++++++++++++++++++++++++++++ Components/ColorArcPicker.qml | 146 +++++++++++++++++----------- Components/FilledSlider.qml | 144 ++++------------------------ Components/GradientSlider.qml | 47 +++++++++ Modules/Drawing/Content.qml | 107 +++++++++++++++++---- 5 files changed, 411 insertions(+), 198 deletions(-) create mode 100644 Components/BaseStyledSlider.qml create mode 100644 Components/GradientSlider.qml diff --git a/Components/BaseStyledSlider.qml b/Components/BaseStyledSlider.qml new file mode 100644 index 0000000..48c8f33 --- /dev/null +++ b/Components/BaseStyledSlider.qml @@ -0,0 +1,165 @@ +import QtQuick +import QtQuick.Templates +import qs.Config + +Slider { + id: root + + property color color: DynamicColors.palette.m3secondary + required property string icon + property bool initialized: false + readonly property bool isHorizontal: orientation === Qt.Horizontal + readonly property bool isVertical: orientation === Qt.Vertical + property real multiplier: 100 + property real oldValue + + // Wrapper components can inject their own track visuals here. + property Component trackContent + + // Keep current behavior for existing usages. + orientation: Qt.Vertical + + background: CustomRect { + id: groove + + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) + height: root.availableHeight + radius: Appearance.rounding.full + width: root.availableWidth + x: root.leftPadding + y: root.topPadding + + Loader { + id: trackLoader + + anchors.fill: parent + sourceComponent: root.trackContent + + onLoaded: { + if (!item) + return; + + item.rootSlider = root; + item.groove = groove; + item.handleItem = handle; + } + } + } + handle: Item { + id: handle + + property alias moving: icon.moving + + implicitHeight: Math.min(root.width, root.height) + implicitWidth: Math.min(root.width, root.height) + x: root.isHorizontal ? root.leftPadding + root.visualPosition * (root.availableWidth - width) : root.leftPadding + (root.availableWidth - width) / 2 + y: root.isVertical ? root.topPadding + root.visualPosition * (root.availableHeight - height) : root.topPadding + (root.availableHeight - height) / 2 + + Elevation { + anchors.fill: parent + level: handleInteraction.containsMouse ? 2 : 1 + radius: rect.radius + } + + CustomRect { + id: rect + + anchors.fill: parent + color: DynamicColors.palette.m3inverseSurface + radius: Appearance.rounding.full + + MouseArea { + id: handleInteraction + + acceptedButtons: Qt.NoButton + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + } + + MaterialIcon { + id: icon + + property bool moving + + function update(): void { + animate = !moving; + binding.when = moving; + font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; + font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; + } + + anchors.centerIn: parent + color: DynamicColors.palette.m3inverseOnSurface + text: root.icon + + onMovingChanged: anim.restart() + + Binding { + id: binding + + property: "text" + target: icon + value: Math.round(root.value * root.multiplier) + when: false + } + + SequentialAnimation { + id: anim + + Anim { + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardAccel + property: "scale" + target: icon + to: 0 + } + + ScriptAction { + script: icon.update() + } + + Anim { + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardDecel + property: "scale" + target: icon + to: 1 + } + } + } + } + } + Behavior on value { + Anim { + duration: Appearance.anim.durations.large + } + } + + onPressedChanged: handle.moving = pressed + onValueChanged: { + if (!initialized) { + initialized = true; + oldValue = value; + return; + } + + if (Math.abs(value - oldValue) < 0.01) + return; + + oldValue = value; + handle.moving = true; + stateChangeDelay.restart(); + } + + Timer { + id: stateChangeDelay + + interval: 500 + + onTriggered: { + if (!root.pressed) + handle.moving = false; + } + } +} diff --git a/Components/ColorArcPicker.qml b/Components/ColorArcPicker.qml index 3e3a182..c909511 100644 --- a/Components/ColorArcPicker.qml +++ b/Components/ColorArcPicker.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import qs.Config Item { id: root @@ -8,11 +9,18 @@ Item { readonly property real arcStartAngle: 0.75 * Math.PI readonly property real arcSweep: 1.5 * Math.PI property real currentHue: 0 + property bool dragActive: false required property var drawing - property real handleSize: 30 + readonly property real handleAngle: hueToAngle(currentHue) + readonly property real handleCenterX: width / 2 + radius * Math.cos(handleAngle) + readonly property real handleCenterY: height / 2 + radius * Math.sin(handleAngle) + property real handleSize: 32 property real lastChromaticHue: 0 - property real ringThickness: 12 - readonly property int segmentCount: 180 + readonly property real radius: (Math.min(width, height) - handleSize) / 2 + readonly property int segmentCount: 240 + readonly property color thumbColor: DynamicColors.palette.m3inverseSurface + readonly property color thumbContentColor: DynamicColors.palette.m3inverseOnSurface + readonly property color trackColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) function hueToAngle(hue) { return arcStartAngle + arcSweep * hue; @@ -26,18 +34,25 @@ Item { return a; } + function pointIsOnTrack(x, y) { + const cx = width / 2; + const cy = height / 2; + const dx = x - cx; + const dy = y - cy; + const distance = Math.sqrt(dx * dx + dy * dy); + + return distance >= radius - handleSize / 2 && distance <= radius + handleSize / 2; + } + function syncFromPenColor() { if (!drawing) return; const c = drawing.penColor; - // QML color exposes HSL channels directly. - // If the current color is chromatic, move the handle to that hue. - // If it is achromatic (black/white/gray), keep the last useful hue. - if (c.hslSaturation > 0) { - currentHue = c.hslHue; - lastChromaticHue = c.hslHue; + if (c.hsvSaturation > 0) { + currentHue = c.hsvHue; + lastChromaticHue = c.hsvHue; } else { currentHue = lastChromaticHue; } @@ -45,16 +60,15 @@ Item { canvas.requestPaint(); } - function updateHueFromPoint(x, y) { - const cx = canvas.width / 2; - const cy = canvas.height / 2; + function updateHueFromPoint(x, y, force = false) { + const cx = width / 2; + const cy = height / 2; const dx = x - cx; const dy = y - cy; const distance = Math.sqrt(dx * dx + dy * dy); - const radius = (Math.min(canvas.width, canvas.height) - handleSize - 8) / 2; - if (distance < radius - 24 || distance > radius + 24) + if (!force && (distance < radius - handleSize / 2 || distance > radius + handleSize / 2)) return; const angle = normalizeAngle(Math.atan2(dy, dx)); @@ -71,7 +85,7 @@ Item { currentHue = relative / arcSweep; lastChromaticHue = currentHue; - drawing.penColor = Qt.hsla(currentHue, 1.0, 0.5, 1.0); + drawing.penColor = Qt.hsva(currentHue, drawing.penColor.hsvSaturation, drawing.penColor.hsvValue, drawing.penColor.a); } implicitHeight: 180 @@ -80,6 +94,9 @@ Item { Component.onCompleted: syncFromPenColor() onCurrentHueChanged: canvas.requestPaint() onDrawingChanged: syncFromPenColor() + onHandleSizeChanged: canvas.requestPaint() + onHeightChanged: canvas.requestPaint() + onWidthChanged: canvas.requestPaint() Connections { function onPenColorChanged() { @@ -97,7 +114,6 @@ Item { renderTarget: Canvas.Image Component.onCompleted: requestPaint() - onHeightChanged: requestPaint() onPaint: { const ctx = getContext("2d"); ctx.reset(); @@ -105,15 +121,10 @@ Item { const cx = width / 2; const cy = height / 2; - const radius = (Math.min(width, height) - root.handleSize - 8) / 2; - - ctx.beginPath(); - ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); - ctx.lineWidth = root.ringThickness + 4; - ctx.lineCap = "round"; - ctx.strokeStyle = Qt.rgba(1, 1, 1, 0.12); - ctx.stroke(); + const radius = root.radius; + const trackWidth = root.handleSize; + // Background track: always show the full hue spectrum for (let i = 0; i < root.segmentCount; ++i) { const t1 = i / root.segmentCount; const t2 = (i + 1) / root.segmentCount; @@ -122,49 +133,76 @@ Item { ctx.beginPath(); ctx.arc(cx, cy, radius, a1, a2); - ctx.lineWidth = root.ringThickness; + ctx.lineWidth = trackWidth; ctx.lineCap = "round"; ctx.strokeStyle = Qt.hsla(t1, 1.0, 0.5, 1.0); ctx.stroke(); } - - const handleAngle = root.hueToAngle(root.currentHue); - const hx = cx + radius * Math.cos(handleAngle); - const hy = cy + radius * Math.sin(handleAngle); - - ctx.beginPath(); - ctx.arc(hx, hy, root.handleSize / 2, 0, Math.PI * 2); - ctx.fillStyle = Qt.rgba(1, 1, 1, 0.95); - ctx.fill(); - - ctx.beginPath(); - ctx.arc(hx, hy, root.handleSize / 2, 0, Math.PI * 2); - ctx.lineWidth = 1.5; - ctx.strokeStyle = Qt.rgba(0, 0, 0, 0.18); - ctx.stroke(); - - ctx.beginPath(); - ctx.arc(hx, hy, root.handleSize / 2 - 6, 0, Math.PI * 2); - ctx.fillStyle = root.drawing.penColor; - ctx.fill(); - - ctx.beginPath(); - ctx.arc(hx, hy, root.handleSize / 2 - 6, 0, Math.PI * 2); - ctx.lineWidth = 1; - ctx.strokeStyle = Qt.rgba(0, 0, 0, 0.20); - ctx.stroke(); } - onWidthChanged: requestPaint() + } + + Item { + id: handle + + height: root.handleSize + width: root.handleSize + x: root.handleCenterX - width / 2 + y: root.handleCenterY - height / 2 + z: 1 + + Elevation { + anchors.fill: parent + level: handleHover.containsMouse ? 2 : 1 + radius: rect.radius + } + + Rectangle { + id: rect + + anchors.fill: parent + color: root.thumbColor + radius: width / 2 + + MouseArea { + id: handleHover + + acceptedButtons: Qt.NoButton + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + } + + Rectangle { + anchors.centerIn: parent + color: root.drawing ? root.drawing.penColor : Qt.hsla(root.currentHue, 1.0, 0.5, 1.0) + height: width + radius: width / 2 + width: parent.width - 12 + } + } } MouseArea { + id: dragArea + acceptedButtons: Qt.LeftButton anchors.fill: parent + hoverEnabled: true + onCanceled: { + root.dragActive = false; + } onPositionChanged: mouse => { - if (mouse.buttons & Qt.LeftButton) + if ((mouse.buttons & Qt.LeftButton) && root.dragActive) + root.updateHueFromPoint(mouse.x, mouse.y, true); + } + onPressed: mouse => { + root.dragActive = root.pointIsOnTrack(mouse.x, mouse.y); + if (root.dragActive) root.updateHueFromPoint(mouse.x, mouse.y); } - onPressed: mouse => root.updateHueFromPoint(mouse.x, mouse.y) + onReleased: { + root.dragActive = false; + } } } diff --git a/Components/FilledSlider.qml b/Components/FilledSlider.qml index bab8051..d38d1cb 100644 --- a/Components/FilledSlider.qml +++ b/Components/FilledSlider.qml @@ -1,141 +1,29 @@ import QtQuick -import QtQuick.Templates import qs.Config -Slider { +BaseStyledSlider { id: root - property color color: DynamicColors.palette.m3secondary - required property string icon - property bool initialized - property real oldValue + trackContent: Component { + Item { + property var groove + readonly property real handleHeight: handleItem ? handleItem.height : 0 + property var handleItem + readonly property real handleWidth: handleItem ? handleItem.width : 0 - orientation: Qt.Vertical - - background: CustomRect { - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.full - - CustomRect { - anchors.left: parent.left - anchors.right: parent.right - color: root.color - implicitHeight: parent.height - y - radius: parent.radius - y: root.handle.y - } - } - handle: Item { - id: handle - - property alias moving: icon.moving - - implicitHeight: root.width - implicitWidth: root.width - y: root.visualPosition * (root.availableHeight - height) - - Elevation { - anchors.fill: parent - level: handleInteraction.containsMouse ? 2 : 1 - radius: rect.radius - } - - CustomRect { - id: rect + // Set by BaseStyledSlider's Loader + property var rootSlider anchors.fill: parent - color: DynamicColors.palette.m3inverseSurface - radius: Appearance.rounding.full - MouseArea { - id: handleInteraction - - acceptedButtons: Qt.NoButton - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true + CustomRect { + color: rootSlider?.color + height: rootSlider?.isVertical ? handleHeight + (1 - rootSlider?.visualPosition) * (groove?.height - handleHeight) : groove?.height + radius: groove?.radius + width: rootSlider?.isHorizontal ? handleWidth + rootSlider?.visualPosition * (groove?.width - handleWidth) : groove?.width + x: rootSlider?.isHorizontal ? (rootSlider?.mirrored ? groove?.width - width : 0) : 0 + y: rootSlider?.isVertical ? groove?.height - height : 0 } - - MaterialIcon { - id: icon - - property bool moving - - function update(): void { - animate = !moving; - binding.when = moving; - font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; - font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; - } - - anchors.centerIn: parent - color: DynamicColors.palette.m3inverseOnSurface - text: root.icon - - onMovingChanged: anim.restart() - - Binding { - id: binding - - property: "text" - target: icon - value: Math.round(root.value * 100) - when: false - } - - SequentialAnimation { - id: anim - - Anim { - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardAccel - property: "scale" - target: icon - to: 0 - } - - ScriptAction { - script: icon.update() - } - - Anim { - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardDecel - property: "scale" - target: icon - to: 1 - } - } - } - } - } - Behavior on value { - Anim { - duration: Appearance.anim.durations.large - } - } - - onPressedChanged: handle.moving = pressed - onValueChanged: { - if (!initialized) { - initialized = true; - return; - } - if (Math.abs(value - oldValue) < 0.01) - return; - oldValue = value; - handle.moving = true; - stateChangeDelay.restart(); - } - - Timer { - id: stateChangeDelay - - interval: 500 - - onTriggered: { - if (!root.pressed) - handle.moving = false; } } } diff --git a/Components/GradientSlider.qml b/Components/GradientSlider.qml new file mode 100644 index 0000000..c19f161 --- /dev/null +++ b/Components/GradientSlider.qml @@ -0,0 +1,47 @@ +import QtQuick +import qs.Config + +BaseStyledSlider { + id: root + + property real alpha: 1.0 + property real brightness: 1.0 + property string channel: "saturation" + readonly property color currentColor: Qt.hsva(hue, channel === "saturation" ? value : saturation, channel === "brightness" ? value : brightness, alpha) + property real hue: 0.0 + property real saturation: 1.0 + + from: 0 + to: 1 + + trackContent: Component { + Item { + property var groove + property var handleItem + property var rootSlider + + anchors.fill: parent + + Rectangle { + anchors.fill: parent + antialiasing: true + color: "transparent" + radius: groove?.radius ?? 0 + + gradient: Gradient { + orientation: rootSlider?.isHorizontal ? Gradient.Horizontal : Gradient.Vertical + + GradientStop { + color: root.channel === "saturation" ? Qt.hsva(root.hue, 0.0, root.brightness, root.alpha) : Qt.hsva(root.hue, root.saturation, 0.0, root.alpha) + position: 0.0 + } + + GradientStop { + color: root.channel === "saturation" ? Qt.hsva(root.hue, 1.0, root.brightness, root.alpha) : Qt.hsva(root.hue, root.saturation, 1.0, root.alpha) + position: 1.0 + } + } + } + } + } +} diff --git a/Modules/Drawing/Content.qml b/Modules/Drawing/Content.qml index 4bcffa0..98c4caf 100644 --- a/Modules/Drawing/Content.qml +++ b/Modules/Drawing/Content.qml @@ -12,10 +12,40 @@ Item { required property Canvas drawing required property var visibilities - implicitHeight: huePicker.implicitHeight + 12 + palette.implicitHeight + Appearance.padding.normal * 2 + function syncFromPenColor() { + if (!drawing) + return; + + if (!saturationSlider.pressed) + saturationSlider.value = drawing.penColor.hsvSaturation; + + if (!brightnessSlider.pressed) + brightnessSlider.value = drawing.penColor.hsvValue; + } + + function updatePenColorFromHsv() { + if (!drawing) + return; + + drawing.penColor = Qt.hsva(huePicker.currentHue, saturationSlider.value, brightnessSlider.value, drawing.penColor.a); + } + + implicitHeight: column.height + Appearance.padding.larger * 2 implicitWidth: huePicker.implicitWidth + Appearance.padding.normal * 2 + Component.onCompleted: syncFromPenColor() + + Connections { + function onPenColorChanged() { + root.syncFromPenColor(); + } + + target: root.drawing + } + Column { + id: column + anchors.centerIn: parent spacing: 12 @@ -25,6 +55,38 @@ Item { drawing: root.drawing } + GradientSlider { + id: saturationSlider + + brightness: brightnessSlider.value + channel: "saturation" + from: 0 + hue: huePicker.currentHue + icon: "\ue40a" + implicitHeight: 30 + implicitWidth: palette.width + orientation: Qt.Horizontal + to: 1 + + onMoved: root.updatePenColorFromHsv() + } + + GradientSlider { + id: brightnessSlider + + channel: "brightness" + from: 0 + hue: huePicker.currentHue + icon: "\ue1ac" + implicitHeight: 30 + implicitWidth: palette.width + orientation: Qt.Horizontal + saturation: saturationSlider.value + to: 1 + + onMoved: root.updatePenColorFromHsv() + } + GridLayout { id: palette @@ -38,12 +100,24 @@ Item { model: root.colors delegate: Item { + id: colorCircle + required property color modelData readonly property bool selected: Qt.colorEqual(root.drawing.penColor, modelData) Layout.fillWidth: true height: 28 + CustomRect { + anchors.centerIn: parent + border.color: Qt.rgba(0, 0, 0, 0.25) + border.width: Qt.colorEqual(modelData, "#ffffff") ? 1 : 0 + color: colorCircle.modelData + height: 20 + radius: width / 2 + width: 20 + } + CustomRect { anchors.centerIn: parent border.color: selected ? "#ffffff" : Qt.rgba(1, 1, 1, 0.28) @@ -52,25 +126,26 @@ Item { height: parent.height radius: width / 2 width: parent.height - } - CustomRect { - anchors.centerIn: parent - border.color: Qt.rgba(0, 0, 0, 0.25) - border.width: Qt.colorEqual(modelData, "#ffffff") ? 1 : 0 - color: modelData - height: 20 - radius: width / 2 - width: 20 - } - - MouseArea { - anchors.fill: parent - - onClicked: root.drawing.penColor = parent.modelData + StateLayer { + onClicked: root.drawing.penColor = colorCircle.modelData + } } } } } + + FilledSlider { + from: 1 + icon: "border_color" + implicitHeight: 30 + implicitWidth: palette.width + multiplier: 1 + orientation: Qt.Horizontal + to: 45 + value: root.drawing.penWidth + + onMoved: root.drawing.penWidth = value + } } } -- 2.47.3 From 3dc6c0ee3e56b2b05ba58f33a974c78f364ae053 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Sun, 15 Mar 2026 22:57:23 +0100 Subject: [PATCH 32/47] remove required property from borders --- Drawers/Windows.qml | 1 - Modules/Bar/Border.qml | 1 - 2 files changed, 2 deletions(-) diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index a7ec012..be40c58 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -146,7 +146,6 @@ Variants { Border { bar: bar - visibilities: visibilities } Backgrounds { diff --git a/Modules/Bar/Border.qml b/Modules/Bar/Border.qml index d12523a..dcf08af 100644 --- a/Modules/Bar/Border.qml +++ b/Modules/Bar/Border.qml @@ -11,7 +11,6 @@ Item { id: root required property Item bar - required property PersistentProperties visibilities anchors.fill: parent -- 2.47.3 From 386040d38a00210191cb795a180991975ac02e59 Mon Sep 17 00:00:00 2001 From: Aram Markarov Date: Mon, 16 Mar 2026 15:11:10 +0100 Subject: [PATCH 33/47] updated plans with current issues to be fixed on settingsWindow --- plans/ideas.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plans/ideas.md b/plans/ideas.md index 941e513..446ee5c 100644 --- a/plans/ideas.md +++ b/plans/ideas.md @@ -21,3 +21,16 @@ - [x] Update module: When there is 1 package it still looks extremely off - [x] Audio module + cava / audio wave ;) ( Don't make it into minecraft blocks but aan actual wave) -- Probably not planned + +# Issues in settingsWindow (16-03-2026) + +- [ ] Drawing tool falls behind when accelerating with the cursor (slow start -> faster movement). +- [ ] Undo option for Drawing tool? +- [ ] Size 1-45 kinda weird numbers (not a real issue = ragebait). +- [ ] Dock has an invisible border it has a visual that it attaches to; perhaps make it visible when the dock shows? +- [ ] Dock apps are clickable and navigates to app (good). If two instances are available, this feels arbitrarily chosen on one instance (maybe defaults to workspace closest to 1?) (like a selection or hover to see options). +- [ ] Dock cannot be closed with escape, user needs to click to leave Dock (Dock stops user from interacting with other apps like typing). +- [ ] Global shortcut for opening Dock and perhaps keyboard navigation? (sounds hard to pull of) +- [ ] If nc or osd global shortcut are used, bar is 100% transparent apart from modules, seems to ignore the regular hover state opacity. +- [ ] Should volume/pipewire module be hover as well? No other bar module is hover apart from the Dock (which is a hidden module activated by hover)? +- [ ] Calendar swipe to other month has no animation -> on purpose? -- 2.47.3 From b555c96de19451d40f472f24deb50c6b9e997b44 Mon Sep 17 00:00:00 2001 From: Aram Markarov Date: Mon, 16 Mar 2026 15:20:51 +0100 Subject: [PATCH 34/47] added question to md file --- plans/ideas.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plans/ideas.md b/plans/ideas.md index 446ee5c..c9c51e2 100644 --- a/plans/ideas.md +++ b/plans/ideas.md @@ -34,3 +34,7 @@ - [ ] If nc or osd global shortcut are used, bar is 100% transparent apart from modules, seems to ignore the regular hover state opacity. - [ ] Should volume/pipewire module be hover as well? No other bar module is hover apart from the Dock (which is a hidden module activated by hover)? - [ ] Calendar swipe to other month has no animation -> on purpose? + +## Additional questions + +- [ ] Can some features be disabled? As in, will they be unloaded from RAM or how is it loaded in memory? Let's say I do not want to use the Dock and Drawing Tool and want to disable them, are they loaded in memory at all? Or all the called upon when the shortcut is hit? -- 2.47.3 From 35fe6c1e5f6ff702bd37f3baf20440f812b7a523 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 16 Mar 2026 15:34:02 +0100 Subject: [PATCH 35/47] added settings options --- Components/CustomSpinBox.qml | 9 +- Components/CustomSplitButtonRow.qml | 5 +- Config/Config.qml | 6 +- Drawers/Interactions.qml | 17 +- Modules/Settings/Categories.qml | 20 +- Modules/Settings/Categories/Appearance.qml | 238 ++++---- Modules/Settings/Categories/Background.qml | 32 +- Modules/Settings/Categories/Bar.qml | 184 ++++++ Modules/Settings/Categories/Dashboard.qml | 212 +++++++ Modules/Settings/Categories/General.qml | 221 +++++++- Modules/Settings/Categories/Launcher.qml | 148 +++++ Modules/Settings/Categories/Lockscreen.qml | 133 +++-- Modules/Settings/Categories/Notifications.qml | 112 ++++ Modules/Settings/Categories/Osd.qml | 77 +++ Modules/Settings/Categories/Services.qml | 120 ++++ Modules/Settings/Categories/Sidebar.qml | 26 + Modules/Settings/Categories/Utilities.qml | 170 ++++++ Modules/Settings/Content.qml | 72 +++ .../Settings/Controls/SettingActionList.qml | 235 ++++++++ .../Settings/Controls/SettingAliasList.qml | 155 ++++++ .../Settings/Controls/SettingBarEntryList.qml | 523 ++++++++++++++++++ Modules/Settings/Controls/SettingInput.qml | 8 +- Modules/Settings/Controls/SettingList.qml | 3 +- Modules/Settings/Controls/SettingReadOnly.qml | 36 ++ Modules/Settings/Controls/SettingSpinBox.qml | 47 ++ Modules/Settings/Controls/SettingSpinner.qml | 1 - .../Settings/Controls/SettingStringList.qml | 41 ++ Modules/Settings/Controls/SettingsHeader.qml | 21 + Modules/Settings/Controls/SettingsPage.qml | 41 ++ Modules/Settings/Controls/SettingsSection.qml | 26 + .../Settings/Controls/StringListEditor.qml | 107 ++++ Modules/TrayItem.qml | 3 +- 32 files changed, 2825 insertions(+), 224 deletions(-) create mode 100644 Modules/Settings/Categories/Bar.qml create mode 100644 Modules/Settings/Categories/Dashboard.qml create mode 100644 Modules/Settings/Categories/Launcher.qml create mode 100644 Modules/Settings/Categories/Notifications.qml create mode 100644 Modules/Settings/Categories/Osd.qml create mode 100644 Modules/Settings/Categories/Services.qml create mode 100644 Modules/Settings/Categories/Sidebar.qml create mode 100644 Modules/Settings/Categories/Utilities.qml create mode 100644 Modules/Settings/Controls/SettingActionList.qml create mode 100644 Modules/Settings/Controls/SettingAliasList.qml create mode 100644 Modules/Settings/Controls/SettingBarEntryList.qml create mode 100644 Modules/Settings/Controls/SettingReadOnly.qml create mode 100644 Modules/Settings/Controls/SettingSpinBox.qml create mode 100644 Modules/Settings/Controls/SettingStringList.qml create mode 100644 Modules/Settings/Controls/SettingsHeader.qml create mode 100644 Modules/Settings/Controls/SettingsPage.qml create mode 100644 Modules/Settings/Controls/SettingsSection.qml create mode 100644 Modules/Settings/Controls/StringListEditor.qml diff --git a/Components/CustomSpinBox.qml b/Components/CustomSpinBox.qml index fe98e72..5a4245b 100644 --- a/Components/CustomSpinBox.qml +++ b/Components/CustomSpinBox.qml @@ -28,6 +28,7 @@ RowLayout { CustomTextField { id: textField + implicitHeight: upButton.implicitHeight inputMethodHints: Qt.ImhFormattedNumbersOnly leftPadding: Appearance.padding.normal padding: Appearance.padding.small @@ -37,7 +38,7 @@ RowLayout { background: CustomRect { color: DynamicColors.tPalette.m3surfaceContainerHigh implicitWidth: 100 - radius: Appearance.rounding.small + radius: Appearance.rounding.full } validator: DoubleValidator { bottom: root.min @@ -82,10 +83,12 @@ RowLayout { } CustomRect { + id: upButton + color: DynamicColors.palette.m3primary implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 implicitWidth: implicitHeight - radius: Appearance.rounding.small + radius: Appearance.rounding.full StateLayer { id: upState @@ -119,7 +122,7 @@ RowLayout { color: DynamicColors.palette.m3primary implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 implicitWidth: implicitHeight - radius: Appearance.rounding.small + radius: Appearance.rounding.full StateLayer { id: downState diff --git a/Components/CustomSplitButtonRow.qml b/Components/CustomSplitButtonRow.qml index a8295e9..491a8ed 100644 --- a/Components/CustomSplitButtonRow.qml +++ b/Components/CustomSplitButtonRow.qml @@ -20,7 +20,7 @@ Item { Layout.fillWidth: true Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 clip: false - z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1 + z: root.expanded ? expandedZ : -1 RowLayout { id: row @@ -36,14 +36,15 @@ Item { color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.larger text: root.label + z: root.expanded ? root.expandedZ : -1 } CustomSplitButton { id: splitButton enabled: root.enabled - menu.z: 1 type: CustomSplitButton.Filled + z: root.expanded ? root.expandedZ : -1 menu.onItemSelected: item => { root.selected(item); diff --git a/Config/Config.qml b/Config/Config.qml index e3b4776..b3f2bac 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -60,8 +60,8 @@ Singleton { } }, anim: { - mediaGifSpeedAdjustment: 300, - sessionGifSpeed: 0.7, + mediaGifSpeedAdjustment: appearance.anim.mediaGifSpeedAdjustment, + sessionGifSpeed: appearance.anim.sessionGifSpeed, durations: { scale: appearance.anim.durations.scale } @@ -189,7 +189,7 @@ Singleton { explorer: general.apps.explorer }, idle: { - timouts: general.idle.timeouts + timeouts: general.idle.timeouts } }; } diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 5116422..e27a765 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -123,6 +123,7 @@ CustomMouseArea { root.dashboardShortcutActive = true; } + root.visibilities.settings = false; root.visibilities.sidebar = false; root.popouts.hasCurrent = false; } else { @@ -149,8 +150,10 @@ CustomMouseArea { } } - if (root.visibilities.launcher) + if (root.visibilities.launcher) { root.visibilities.dock = false; + root.visibilities.settings = false; + } } function onOsdChanged() { @@ -168,6 +171,18 @@ CustomMouseArea { if (root.visibilities.resources && root.popouts.currentName.startsWith("audio")) { root.popouts.hasCurrent = false; } + + if (root.visibilities.resources) + root.visibilities.settings = false; + } + + function onSettingsChanged() { + if (root.visibilities.settings) { + root.visibilities.resources = false; + root.visibilities.dashboard = false; + root.panels.popouts.hasCurrent = false; + root.visibilities.launcher = false; + } } function onSidebarChanged() { diff --git a/Modules/Settings/Categories.qml b/Modules/Settings/Categories.qml index bd4da4e..731428c 100644 --- a/Modules/Settings/Categories.qml +++ b/Modules/Settings/Categories.qml @@ -22,68 +22,75 @@ Item { ListElement { icon: "settings" + key: "general" name: "General" } ListElement { icon: "wallpaper" + key: "wallpaper" name: "Wallpaper" } ListElement { icon: "settop_component" + key: "bar" name: "Bar" } ListElement { icon: "lock" + key: "lockscreen" name: "Lockscreen" } ListElement { icon: "build_circle" + key: "services" name: "Services" } ListElement { icon: "notifications" + key: "notifications" name: "Notifications" } ListElement { icon: "view_sidebar" + key: "sidebar" name: "Sidebar" } ListElement { icon: "handyman" + key: "utilities" name: "Utilities" } ListElement { icon: "dashboard" + key: "dashboard" name: "Dashboard" } ListElement { icon: "colors" + key: "appearance" name: "Appearance" } ListElement { icon: "display_settings" + key: "osd" name: "On screen display" } ListElement { icon: "rocket_launch" + key: "launcher" name: "Launcher" } - - ListElement { - icon: "colors" - name: "Colors" - } } CustomClippingRect { @@ -129,6 +136,7 @@ Item { required property string icon required property int index + required property string key required property string name implicitHeight: 42 @@ -172,7 +180,7 @@ Item { id: layer onClicked: { - root.content.currentCategory = categoryItem.name.toLowerCase(); + root.content.currentCategory = categoryItem.key; clayout.currentIndex = categoryItem.index; } } diff --git a/Modules/Settings/Categories/Appearance.qml b/Modules/Settings/Categories/Appearance.qml index 1c6d237..01c88e5 100644 --- a/Modules/Settings/Categories/Appearance.qml +++ b/Modules/Settings/Categories/Appearance.qml @@ -1,141 +1,159 @@ -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Modules as Modules import qs.Modules.Settings.Controls import qs.Config -import qs.Helpers -CustomFlickable { +SettingsPage { id: root - contentHeight: clayout.implicitHeight + SettingsSection { + SettingsHeader { + name: "Scale" + } - TapHandler { - acceptedButtons: Qt.LeftButton + SettingSpinBox { + name: "Rounding scale" + object: Config.appearance.rounding + setting: "scale" + step: 0.1 + } - onTapped: function (eventPoint) { - const menu = SettingsDropdowns.activeMenu; - if (!menu) - return; + Separator { + } - const p = eventPoint.scenePosition; + SettingSpinBox { + name: "Spacing scale" + object: Config.appearance.spacing + setting: "scale" + step: 0.1 + } - if (SettingsDropdowns.hit(SettingsDropdowns.activeTrigger, p)) - return; + Separator { + } - if (SettingsDropdowns.hit(menu, p)) - return; + SettingSpinBox { + name: "Padding scale" + object: Config.appearance.padding + setting: "scale" + step: 0.1 + } - SettingsDropdowns.closeActive(); + Separator { + } + + SettingSpinBox { + name: "Font size scale" + object: Config.appearance.font.size + setting: "scale" + step: 0.1 + } + + Separator { + } + + SettingSpinBox { + name: "Animation duration scale" + object: Config.appearance.anim.durations + setting: "scale" + step: 0.1 } } - ColumnLayout { - id: clayout + SettingsSection { + SettingsHeader { + name: "Fonts" + } - anchors.left: parent.left - anchors.right: parent.right + SettingInput { + name: "Sans family" + object: Config.appearance.font.family + setting: "sans" + } - CustomRect { - Layout.fillWidth: true - Layout.preferredHeight: colorLayout.implicitHeight + Appearance.padding.normal * 2 - color: DynamicColors.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - Appearance.padding.smaller + Separator { + } - ColumnLayout { - id: colorLayout + SettingInput { + name: "Monospace family" + object: Config.appearance.font.family + setting: "mono" + } - anchors.left: parent.left - anchors.margins: Appearance.padding.large - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: Appearance.spacing.normal + Separator { + } - Settings { - name: "Color" - } + SettingInput { + name: "Material family" + object: Config.appearance.font.family + setting: "material" + } - SettingSwitch { - name: "Automatic color scheme" - object: Config.general.color - setting: "schemeGeneration" - } + Separator { + } - Separator { - } - - SettingSwitch { - name: "Smart color scheme" - object: Config.general.color - setting: "smart" - } - - Separator { - } - - SettingSpinner { - name: "Schedule dark mode" - object: Config.general.color - settings: ["scheduleDarkStart", "scheduleDarkEnd"] - z: 2 - } - - Separator { - } - - CustomSplitButtonRow { - enabled: true - label: qsTr("Scheme mode") - - menuItems: [ - MenuItem { - property string val: "light" - - icon: "light_mode" - text: qsTr("Light") - }, - MenuItem { - property string val: "dark" - - icon: "dark_mode" - text: qsTr("Dark") - } - ] - - Component.onCompleted: { - if (Config.general.color.mode === "light") - active = menuItems[0]; - else - active = menuItems[1]; - } - onSelected: item => { - Config.general.color.mode = item.val; - Config.save(); - } - } - } + SettingInput { + name: "Clock family" + object: Config.appearance.font.family + setting: "clock" } } - component Settings: CustomRect { - id: settingsItem + SettingsSection { + SettingsHeader { + name: "Animation" + } - required property string name + SettingSpinBox { + name: "Media GIF speed adjustment" + object: Config.appearance.anim + setting: "mediaGifSpeedAdjustment" + step: 10 + } - Layout.preferredHeight: 60 - Layout.preferredWidth: 200 + Separator { + } - CustomText { - id: text + SettingSpinBox { + name: "Session GIF speed" + max: 5 + min: 0 + object: Config.appearance.anim + setting: "sessionGifSpeed" + step: 0.1 + } + } - anchors.fill: parent - font.bold: true - font.pointSize: Appearance.font.size.large * 2 - text: settingsItem.name - verticalAlignment: Text.AlignVCenter + SettingsSection { + SettingsHeader { + name: "Transparency" + } + + SettingSwitch { + name: "Enable transparency" + object: Config.appearance.transparency + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Base opacity" + max: 1 + min: 0 + object: Config.appearance.transparency + setting: "base" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Layer opacity" + max: 1 + min: 0 + object: Config.appearance.transparency + setting: "layers" + step: 0.05 } } } diff --git a/Modules/Settings/Categories/Background.qml b/Modules/Settings/Categories/Background.qml index 2d24d68..b7fabb9 100644 --- a/Modules/Settings/Categories/Background.qml +++ b/Modules/Settings/Categories/Background.qml @@ -1,13 +1,29 @@ -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Modules as Modules +import qs.Modules.Settings.Controls import qs.Config -import qs.Helpers -CustomRect { +SettingsPage { id: root + SettingsSection { + SettingsHeader { + name: "Wallpaper" + } + + SettingSwitch { + name: "Enable wallpaper rendering" + object: Config.background + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Fade duration" + min: 0 + object: Config.background + setting: "wallFadeDuration" + step: 50 + } + } } diff --git a/Modules/Settings/Categories/Bar.qml b/Modules/Settings/Categories/Bar.qml new file mode 100644 index 0000000..150a709 --- /dev/null +++ b/Modules/Settings/Categories/Bar.qml @@ -0,0 +1,184 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Bar" + } + + SettingSwitch { + name: "Auto hide" + object: Config.barConfig + setting: "autoHide" + } + + Separator { + } + + SettingSpinBox { + name: "Height" + min: 1 + object: Config.barConfig + setting: "height" + } + + Separator { + } + + SettingSpinBox { + name: "Rounding" + min: 0 + object: Config.barConfig + setting: "rounding" + } + + Separator { + } + + SettingSpinBox { + name: "Border" + min: 0 + object: Config.barConfig + setting: "border" + } + } + + SettingsSection { + SettingsHeader { + name: "Popouts" + } + + SettingSwitch { + name: "Tray" + object: Config.barConfig.popouts + setting: "tray" + } + + Separator { + } + + SettingSwitch { + name: "Audio" + object: Config.barConfig.popouts + setting: "audio" + } + + Separator { + } + + SettingSwitch { + name: "Active window" + object: Config.barConfig.popouts + setting: "activeWindow" + } + + Separator { + } + + SettingSwitch { + name: "Resources" + object: Config.barConfig.popouts + setting: "resources" + } + + Separator { + } + + SettingSwitch { + name: "Clock" + object: Config.barConfig.popouts + setting: "clock" + } + + Separator { + } + + SettingSwitch { + name: "Network" + object: Config.barConfig.popouts + setting: "network" + } + + Separator { + } + + SettingSwitch { + name: "Power" + object: Config.barConfig.popouts + setting: "upower" + } + } + + SettingsSection { + SettingsHeader { + name: "Entries" + } + + SettingBarEntryList { + name: "Bar entries" + object: Config.barConfig + setting: "entries" + } + } + + SettingsSection { + SettingsHeader { + name: "Dock" + } + + SettingSwitch { + name: "Enable dock" + object: Config.dock + setting: "enable" + } + + Separator { + } + + SettingSpinBox { + name: "Dock height" + min: 1 + object: Config.dock + setting: "height" + } + + Separator { + } + + SettingSwitch { + name: "Hover to reveal" + object: Config.dock + setting: "hoverToReveal" + } + + Separator { + } + + SettingSwitch { + name: "Pin on startup" + object: Config.dock + setting: "pinnedOnStartup" + } + + Separator { + } + + SettingStringList { + name: "Pinned apps" + addLabel: qsTr("Add pinned app") + object: Config.dock + setting: "pinnedApps" + } + + Separator { + } + + SettingStringList { + name: "Ignored app regexes" + addLabel: qsTr("Add ignored regex") + object: Config.dock + setting: "ignoredAppRegexes" + } + } +} diff --git a/Modules/Settings/Categories/Dashboard.qml b/Modules/Settings/Categories/Dashboard.qml new file mode 100644 index 0000000..f597541 --- /dev/null +++ b/Modules/Settings/Categories/Dashboard.qml @@ -0,0 +1,212 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Dashboard" + } + + SettingSwitch { + name: "Enable dashboard" + object: Config.dashboard + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Media update interval" + min: 0 + object: Config.dashboard + setting: "mediaUpdateInterval" + step: 50 + } + + Separator { + } + + SettingSpinBox { + name: "Resource update interval" + min: 0 + object: Config.dashboard + setting: "resourceUpdateInterval" + step: 50 + } + + Separator { + } + + SettingSpinBox { + name: "Drag threshold" + min: 0 + object: Config.dashboard + setting: "dragThreshold" + } + } + + SettingsSection { + SettingsHeader { + name: "Performance" + } + + SettingSwitch { + name: "Show battery" + object: Config.dashboard.performance + setting: "showBattery" + } + + Separator { + } + + SettingSwitch { + name: "Show GPU" + object: Config.dashboard.performance + setting: "showGpu" + } + + Separator { + } + + SettingSwitch { + name: "Show CPU" + object: Config.dashboard.performance + setting: "showCpu" + } + + Separator { + } + + SettingSwitch { + name: "Show memory" + object: Config.dashboard.performance + setting: "showMemory" + } + + Separator { + } + + SettingSwitch { + name: "Show storage" + object: Config.dashboard.performance + setting: "showStorage" + } + + Separator { + } + + SettingSwitch { + name: "Show network" + object: Config.dashboard.performance + setting: "showNetwork" + } + } + + SettingsSection { + SettingsHeader { + name: "Layout Sizes" + } + + SettingReadOnly { + name: "Tab indicator height" + value: String(Config.dashboard.sizes.tabIndicatorHeight) + } + + Separator { + } + + SettingReadOnly { + name: "Tab indicator spacing" + value: String(Config.dashboard.sizes.tabIndicatorSpacing) + } + + Separator { + } + + SettingReadOnly { + name: "Info width" + value: String(Config.dashboard.sizes.infoWidth) + } + + Separator { + } + + SettingReadOnly { + name: "Info icon size" + value: String(Config.dashboard.sizes.infoIconSize) + } + + Separator { + } + + SettingReadOnly { + name: "Date time width" + value: String(Config.dashboard.sizes.dateTimeWidth) + } + + Separator { + } + + SettingReadOnly { + name: "Media width" + value: String(Config.dashboard.sizes.mediaWidth) + } + + Separator { + } + + SettingReadOnly { + name: "Media progress sweep" + value: String(Config.dashboard.sizes.mediaProgressSweep) + } + + Separator { + } + + SettingReadOnly { + name: "Media progress thickness" + value: String(Config.dashboard.sizes.mediaProgressThickness) + } + + Separator { + } + + SettingReadOnly { + name: "Resource progress thickness" + value: String(Config.dashboard.sizes.resourceProgessThickness) + } + + Separator { + } + + SettingReadOnly { + name: "Weather width" + value: String(Config.dashboard.sizes.weatherWidth) + } + + Separator { + } + + SettingReadOnly { + name: "Media cover art size" + value: String(Config.dashboard.sizes.mediaCoverArtSize) + } + + Separator { + } + + SettingReadOnly { + name: "Media visualiser size" + value: String(Config.dashboard.sizes.mediaVisualiserSize) + } + + Separator { + } + + SettingReadOnly { + name: "Resource size" + value: String(Config.dashboard.sizes.resourceSize) + } + } +} diff --git a/Modules/Settings/Categories/General.qml b/Modules/Settings/Categories/General.qml index e35999b..0c0bf0b 100644 --- a/Modules/Settings/Categories/General.qml +++ b/Modules/Settings/Categories/General.qml @@ -1,37 +1,212 @@ -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Modules as Modules +import qs.Modules.Settings.Controls import qs.Config -import qs.Helpers +import qs.Components -CustomRect { +SettingsPage { id: root - ColumnLayout { - id: clayout + function schemeTypeItem(items, value) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.value === value) + return item; + } - anchors.fill: parent + return items[0] ?? null; } - component Settings: CustomRect { - id: settingsItem + SettingsSection { + SettingsHeader { + name: "General" + } - required property string name + SettingInput { + name: "Logo" + object: Config.general + setting: "logo" + } - Layout.preferredHeight: 60 - Layout.preferredWidth: 200 + Separator { + } - CustomText { - id: text + SettingInput { + name: "Wallpaper path" + object: Config.general + setting: "wallpaperPath" + } - anchors.fill: parent - font.bold: true - font.pointSize: Appearance.font.size.large * 2 - text: settingsItem.name - verticalAlignment: Text.AlignVCenter + Separator { + } + + SettingSwitch { + name: "Desktop icons" + object: Config.general + setting: "desktopIcons" + } + } + + SettingsSection { + SettingsHeader { + name: "Color" + } + + CustomSplitButtonRow { + active: Config.general.color.mode === "light" ? menuItems[0] : menuItems[1] + label: qsTr("Scheme mode") + + menuItems: [ + MenuItem { + icon: "light_mode" + text: qsTr("Light") + value: "light" + }, + MenuItem { + icon: "dark_mode" + text: qsTr("Dark") + value: "dark" + } + ] + + onSelected: item => { + Config.general.color.mode = item.value; + Config.save(); + } + } + + Separator { + } + + CustomSplitButtonRow { + id: schemeType + + active: root.schemeTypeItem(menuItems, Config.colors.schemeType) + label: qsTr("Scheme type") + z: 2 + + menuItems: [ + MenuItem { + icon: "palette" + text: qsTr("Vibrant") + value: "vibrant" + }, + MenuItem { + icon: "gesture" + text: qsTr("Expressive") + value: "expressive" + }, + MenuItem { + icon: "contrast" + text: qsTr("Monochrome") + value: "monochrome" + }, + MenuItem { + icon: "tonality" + text: qsTr("Neutral") + value: "neutral" + }, + MenuItem { + icon: "gradient" + text: qsTr("Tonal spot") + value: "tonalSpot" + }, + MenuItem { + icon: "target" + text: qsTr("Fidelity") + value: "fidelity" + }, + MenuItem { + icon: "article" + text: qsTr("Content") + value: "content" + }, + MenuItem { + icon: "colors" + text: qsTr("Rainbow") + value: "rainbow" + }, + MenuItem { + icon: "nutrition" + text: qsTr("Fruit salad") + value: "fruitSalad" + } + ] + + onSelected: item => { + Config.colors.schemeType = item.value; + Config.save(); + } + } + + Separator { + } + + SettingSwitch { + name: "Automatic color scheme" + object: Config.general.color + setting: "schemeGeneration" + } + + Separator { + } + + SettingSwitch { + name: "Smart color scheme" + object: Config.general.color + setting: "smart" + } + + Separator { + } + + SettingSpinner { + name: "Schedule dark mode" + object: Config.general.color + settings: ["scheduleDarkStart", "scheduleDarkEnd"] + } + } + + SettingsSection { + z: -1 + + SettingsHeader { + name: "Default Apps" + } + + SettingStringList { + addLabel: qsTr("Add terminal command") + name: "Terminal" + object: Config.general.apps + setting: "terminal" + } + + Separator { + } + + SettingStringList { + addLabel: qsTr("Add audio command") + name: "Audio" + object: Config.general.apps + setting: "audio" + } + + Separator { + } + + SettingStringList { + addLabel: qsTr("Add playback command") + name: "Playback" + object: Config.general.apps + setting: "playback" + } + + Separator { + } + + SettingStringList { + addLabel: qsTr("Add explorer command") + name: "Explorer" + object: Config.general.apps + setting: "explorer" } } } diff --git a/Modules/Settings/Categories/Launcher.qml b/Modules/Settings/Categories/Launcher.qml new file mode 100644 index 0000000..18cb34f --- /dev/null +++ b/Modules/Settings/Categories/Launcher.qml @@ -0,0 +1,148 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Launcher" + } + + SettingSpinBox { + name: "Max apps shown" + min: 1 + object: Config.launcher + setting: "maxAppsShown" + } + + Separator { + } + + SettingSpinBox { + name: "Max wallpapers shown" + min: 1 + object: Config.launcher + setting: "maxWallpapers" + } + + Separator { + } + + SettingInput { + name: "Action prefix" + object: Config.launcher + setting: "actionPrefix" + } + + Separator { + } + + SettingInput { + name: "Special prefix" + object: Config.launcher + setting: "specialPrefix" + } + } + + SettingsSection { + SettingsHeader { + name: "Fuzzy Search" + } + + SettingSwitch { + name: "Apps" + object: Config.launcher.useFuzzy + setting: "apps" + } + + Separator { + } + + SettingSwitch { + name: "Actions" + object: Config.launcher.useFuzzy + setting: "actions" + } + + Separator { + } + + SettingSwitch { + name: "Schemes" + object: Config.launcher.useFuzzy + setting: "schemes" + } + + Separator { + } + + SettingSwitch { + name: "Variants" + object: Config.launcher.useFuzzy + setting: "variants" + } + + Separator { + } + + SettingSwitch { + name: "Wallpapers" + object: Config.launcher.useFuzzy + setting: "wallpapers" + } + } + + SettingsSection { + SettingsHeader { + name: "Sizes" + } + + SettingSpinBox { + name: "Item width" + min: 1 + object: Config.launcher.sizes + setting: "itemWidth" + } + + Separator { + } + + SettingSpinBox { + name: "Item height" + min: 1 + object: Config.launcher.sizes + setting: "itemHeight" + } + + Separator { + } + + SettingSpinBox { + name: "Wallpaper width" + min: 1 + object: Config.launcher.sizes + setting: "wallpaperWidth" + } + + Separator { + } + + SettingSpinBox { + name: "Wallpaper height" + min: 1 + object: Config.launcher.sizes + setting: "wallpaperHeight" + } + } + + SettingsSection { + SettingsHeader { + name: "Actions" + } + + SettingActionList { + name: "Launcher actions" + object: Config.launcher + setting: "actions" + } + } +} diff --git a/Modules/Settings/Categories/Lockscreen.qml b/Modules/Settings/Categories/Lockscreen.qml index 570aecc..7c9d2d2 100644 --- a/Modules/Settings/Categories/Lockscreen.qml +++ b/Modules/Settings/Categories/Lockscreen.qml @@ -1,77 +1,90 @@ -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Modules as Modules import qs.Modules.Settings.Categories.Lockscreen +import qs.Modules.Settings.Controls import qs.Config -import qs.Helpers -CustomFlickable { +SettingsPage { id: root - contentHeight: clayout.implicitHeight + SettingsSection { + SettingsHeader { + name: "Lockscreen" + } - TapHandler { - acceptedButtons: Qt.LeftButton + SettingSwitch { + name: "Recolor logo" + object: Config.lock + setting: "recolorLogo" + } - onTapped: function (eventPoint) { - const menu = SettingsDropdowns.activeMenu; - if (!menu) - return; + Separator { + } - const p = eventPoint.scenePosition; + SettingSwitch { + name: "Enable fingerprint" + object: Config.lock + setting: "enableFprint" + } - if (SettingsDropdowns.hit(SettingsDropdowns.activeTrigger, p)) - return; + Separator { + } - if (SettingsDropdowns.hit(menu, p)) - return; + SettingSpinBox { + name: "Max fingerprint tries" + min: 1 + object: Config.lock + setting: "maxFprintTries" + step: 1 + } - SettingsDropdowns.closeActive(); + Separator { + } + + SettingSpinBox { + name: "Blur amount" + min: 0 + object: Config.lock + setting: "blurAmount" + step: 1 + } + + Separator { + } + + SettingSpinBox { + name: "Height multiplier" + max: 2 + min: 0.1 + object: Config.lock.sizes + setting: "heightMult" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Aspect ratio" + max: 4 + min: 0.5 + object: Config.lock.sizes + setting: "ratio" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Center width" + min: 100 + object: Config.lock.sizes + setting: "centerWidth" + step: 10 } } - ColumnLayout { - id: clayout - - anchors.fill: parent - - CustomRect { - Layout.fillWidth: true - Layout.preferredHeight: idleLayout.implicitHeight + Appearance.padding.normal * 2 - color: DynamicColors.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - Appearance.padding.smaller - z: -1 - - Idle { - id: idleLayout - - anchors.left: parent.left - anchors.margins: Appearance.padding.large - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - } - } - } - - component Settings: CustomRect { - id: settingsItem - - required property string name - - Layout.preferredHeight: 60 - Layout.preferredWidth: 200 - - CustomText { - id: text - - anchors.fill: parent - font.bold: true - font.pointSize: Appearance.font.size.large * 2 - text: settingsItem.name - verticalAlignment: Text.AlignVCenter + SettingsSection { + Idle { } } } diff --git a/Modules/Settings/Categories/Notifications.qml b/Modules/Settings/Categories/Notifications.qml new file mode 100644 index 0000000..ef0486b --- /dev/null +++ b/Modules/Settings/Categories/Notifications.qml @@ -0,0 +1,112 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Notifications" + } + + SettingSwitch { + name: "Expire notifications" + object: Config.notifs + setting: "expire" + } + + Separator { + } + + SettingSpinBox { + name: "Default expire timeout" + min: 0 + object: Config.notifs + setting: "defaultExpireTimeout" + step: 100 + } + + Separator { + } + + SettingSpinBox { + name: "App notification cooldown" + min: 0 + object: Config.notifs + setting: "appNotifCooldown" + step: 100 + } + + Separator { + } + + SettingSpinBox { + name: "Clear threshold" + max: 1 + min: 0 + object: Config.notifs + setting: "clearThreshold" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Expand threshold" + min: 0 + object: Config.notifs + setting: "expandThreshold" + } + + Separator { + } + + SettingSwitch { + name: "Action on click" + object: Config.notifs + setting: "actionOnClick" + } + + Separator { + } + + SettingSpinBox { + name: "Group preview count" + min: 1 + object: Config.notifs + setting: "groupPreviewNum" + } + } + + SettingsSection { + SettingsHeader { + name: "Sizes" + } + + SettingSpinBox { + name: "Width" + min: 1 + object: Config.notifs.sizes + setting: "width" + } + + Separator { + } + + SettingSpinBox { + name: "Image size" + min: 1 + object: Config.notifs.sizes + setting: "image" + } + + Separator { + } + + SettingSpinBox { + name: "Badge size" + min: 1 + object: Config.notifs.sizes + setting: "badge" + } + } +} diff --git a/Modules/Settings/Categories/Osd.qml b/Modules/Settings/Categories/Osd.qml new file mode 100644 index 0000000..34d5724 --- /dev/null +++ b/Modules/Settings/Categories/Osd.qml @@ -0,0 +1,77 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "On Screen Display" + } + + SettingSwitch { + name: "Enable OSD" + object: Config.osd + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Hide delay" + min: 0 + object: Config.osd + setting: "hideDelay" + step: 100 + } + + Separator { + } + + SettingSwitch { + name: "Enable brightness OSD" + object: Config.osd + setting: "enableBrightness" + } + + Separator { + } + + SettingSwitch { + name: "Enable microphone OSD" + object: Config.osd + setting: "enableMicrophone" + } + + Separator { + } + + SettingSwitch { + name: "Brightness on all monitors" + object: Config.osd + setting: "allMonBrightness" + } + } + + SettingsSection { + SettingsHeader { + name: "Sizes" + } + + SettingSpinBox { + name: "Slider width" + min: 1 + object: Config.osd.sizes + setting: "sliderWidth" + } + + Separator { + } + + SettingSpinBox { + name: "Slider height" + min: 1 + object: Config.osd.sizes + setting: "sliderHeight" + } + } +} diff --git a/Modules/Settings/Categories/Services.qml b/Modules/Settings/Categories/Services.qml new file mode 100644 index 0000000..bae8112 --- /dev/null +++ b/Modules/Settings/Categories/Services.qml @@ -0,0 +1,120 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Services" + } + + SettingInput { + name: "Weather location" + object: Config.services + setting: "weatherLocation" + } + + Separator { + } + + SettingSwitch { + name: "Use Fahrenheit" + object: Config.services + setting: "useFahrenheit" + } + + Separator { + } + + SettingSwitch { + name: "Use twelve hour clock" + object: Config.services + setting: "useTwelveHourClock" + } + + Separator { + } + + SettingSwitch { + name: "Enable ddcutil service" + object: Config.services + setting: "ddcutilService" + } + + Separator { + } + + SettingInput { + name: "GPU type" + object: Config.services + setting: "gpuType" + } + } + + SettingsSection { + SettingsHeader { + name: "Media" + } + + SettingSpinBox { + name: "Audio increment" + max: 1 + min: 0 + object: Config.services + setting: "audioIncrement" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Brightness increment" + max: 1 + min: 0 + object: Config.services + setting: "brightnessIncrement" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Max volume" + max: 5 + min: 0 + object: Config.services + setting: "maxVolume" + step: 0.05 + } + + Separator { + } + + SettingInput { + name: "Default player" + object: Config.services + setting: "defaultPlayer" + } + + Separator { + } + + SettingSpinBox { + name: "Visualizer bars" + min: 1 + object: Config.services + setting: "visualizerBars" + step: 1 + } + + Separator { + } + + SettingAliasList { + name: "Player aliases" + object: Config.services + setting: "playerAliases" + } + } +} diff --git a/Modules/Settings/Categories/Sidebar.qml b/Modules/Settings/Categories/Sidebar.qml new file mode 100644 index 0000000..b661e04 --- /dev/null +++ b/Modules/Settings/Categories/Sidebar.qml @@ -0,0 +1,26 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Sidebar" + } + + SettingSwitch { + name: "Enable sidebar" + object: Config.sidebar + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Width" + min: 1 + object: Config.sidebar.sizes + setting: "width" + } + } +} diff --git a/Modules/Settings/Categories/Utilities.qml b/Modules/Settings/Categories/Utilities.qml new file mode 100644 index 0000000..3ec1e15 --- /dev/null +++ b/Modules/Settings/Categories/Utilities.qml @@ -0,0 +1,170 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Utilities" + } + + SettingSwitch { + name: "Enable utilities" + object: Config.utilities + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Max toasts" + min: 1 + object: Config.utilities + setting: "maxToasts" + } + + Separator { + } + + SettingSpinBox { + name: "Panel width" + min: 1 + object: Config.utilities.sizes + setting: "width" + } + + Separator { + } + + SettingSpinBox { + name: "Toast width" + min: 1 + object: Config.utilities.sizes + setting: "toastWidth" + } + } + + SettingsSection { + SettingsHeader { + name: "Toasts" + } + + SettingSwitch { + name: "Config loaded" + object: Config.utilities.toasts + setting: "configLoaded" + } + + Separator { + } + + SettingSwitch { + name: "Charging changed" + object: Config.utilities.toasts + setting: "chargingChanged" + } + + Separator { + } + + SettingSwitch { + name: "Game mode changed" + object: Config.utilities.toasts + setting: "gameModeChanged" + } + + Separator { + } + + SettingSwitch { + name: "Do not disturb changed" + object: Config.utilities.toasts + setting: "dndChanged" + } + + Separator { + } + + SettingSwitch { + name: "Audio output changed" + object: Config.utilities.toasts + setting: "audioOutputChanged" + } + + Separator { + } + + SettingSwitch { + name: "Audio input changed" + object: Config.utilities.toasts + setting: "audioInputChanged" + } + + Separator { + } + + SettingSwitch { + name: "Caps lock changed" + object: Config.utilities.toasts + setting: "capsLockChanged" + } + + Separator { + } + + SettingSwitch { + name: "Num lock changed" + object: Config.utilities.toasts + setting: "numLockChanged" + } + + Separator { + } + + SettingSwitch { + name: "Keyboard layout changed" + object: Config.utilities.toasts + setting: "kbLayoutChanged" + } + + Separator { + } + + SettingSwitch { + name: "VPN changed" + object: Config.utilities.toasts + setting: "vpnChanged" + } + + Separator { + } + + SettingSwitch { + name: "Now playing" + object: Config.utilities.toasts + setting: "nowPlaying" + } + } + + SettingsSection { + SettingsHeader { + name: "VPN" + } + + SettingSwitch { + name: "Enable VPN integration" + object: Config.utilities.vpn + setting: "enabled" + } + + Separator { + } + + SettingStringList { + name: "Provider" + addLabel: qsTr("Add VPN provider") + object: Config.utilities.vpn + setting: "provider" + } + } +} diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml index 3bb23a2..0e47180 100644 --- a/Modules/Settings/Content.qml +++ b/Modules/Settings/Content.qml @@ -27,10 +27,26 @@ Item { stack.push(general); else if (currentCategory === "wallpaper") stack.push(background); + else if (currentCategory === "bar") + stack.push(bar); else if (currentCategory === "appearance") stack.push(appearance); else if (currentCategory === "lockscreen") stack.push(lockscreen); + else if (currentCategory === "services") + stack.push(services); + else if (currentCategory === "notifications") + stack.push(notifications); + else if (currentCategory === "sidebar") + stack.push(sidebar); + else if (currentCategory === "utilities") + stack.push(utilities); + else if (currentCategory === "dashboard") + stack.push(dashboard); + else if (currentCategory === "osd") + stack.push(osd); + else if (currentCategory === "launcher") + stack.push(launcher); } target: root @@ -101,10 +117,66 @@ Item { } } + Component { + id: bar + + Cat.Bar { + } + } + Component { id: lockscreen Cat.Lockscreen { } } + + Component { + id: services + + Cat.Services { + } + } + + Component { + id: notifications + + Cat.Notifications { + } + } + + Component { + id: sidebar + + Cat.Sidebar { + } + } + + Component { + id: utilities + + Cat.Utilities { + } + } + + Component { + id: dashboard + + Cat.Dashboard { + } + } + + Component { + id: osd + + Cat.Osd { + } + } + + Component { + id: launcher + + Cat.Launcher { + } + } } diff --git a/Modules/Settings/Controls/SettingActionList.qml b/Modules/Settings/Controls/SettingActionList.qml new file mode 100644 index 0000000..90ccac5 --- /dev/null +++ b/Modules/Settings/Controls/SettingActionList.qml @@ -0,0 +1,235 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +ColumnLayout { + id: root + + required property string name + required property var object + required property string setting + + function addAction() { + const list = [...root.object[root.setting]]; + list.push({ + name: "New Action", + icon: "bolt", + description: "", + command: [], + enabled: true, + dangerous: false + }); + root.object[root.setting] = list; + Config.save(); + } + + function removeAction(index) { + const list = [...root.object[root.setting]]; + list.splice(index, 1); + root.object[root.setting] = list; + Config.save(); + } + + function updateAction(index, key, value) { + const list = [...root.object[root.setting]]; + const entry = list[index]; + entry[key] = value; + list[index] = entry; + root.object[root.setting] = list; + Config.save(); + } + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + Repeater { + model: [...root.object[root.setting]] + + CustomRect { + required property int index + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: layout.implicitHeight + Appearance.padding.normal * 2 + color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.margins: Appearance.padding.normal + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: modelData.name ?? qsTr("Action") + } + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "delete" + type: IconButton.Tonal + + onClicked: root.removeAction(index) + } + } + + Separator { + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Name") + } + + CustomRect { + Layout.preferredHeight: 33 + Layout.preferredWidth: 350 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.name ?? "" + + onEditingFinished: root.updateAction(index, "name", text) + } + } + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Icon") + } + + CustomRect { + Layout.preferredHeight: 33 + Layout.preferredWidth: 350 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.icon ?? "" + + onEditingFinished: root.updateAction(index, "icon", text) + } + } + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Description") + } + + CustomRect { + Layout.preferredHeight: 33 + Layout.preferredWidth: 350 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.description ?? "" + + onEditingFinished: root.updateAction(index, "description", text) + } + } + } + + StringListEditor { + Layout.fillWidth: true + addLabel: qsTr("Add command argument") + values: [...(modelData.command ?? [])] + + onListEdited: function (values) { + root.updateAction(index, "command", values); + } + } + + Separator { + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Enabled") + } + + CustomSwitch { + checked: modelData.enabled ?? true + + onToggled: root.updateAction(index, "enabled", checked) + } + } + + Separator { + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Dangerous") + } + + CustomSwitch { + checked: modelData.dangerous ?? false + + onToggled: root.updateAction(index, "dangerous", checked) + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: root.addAction() + } + + CustomText { + Layout.fillWidth: true + text: qsTr("Add action") + } + } +} diff --git a/Modules/Settings/Controls/SettingAliasList.qml b/Modules/Settings/Controls/SettingAliasList.qml new file mode 100644 index 0000000..f7f8959 --- /dev/null +++ b/Modules/Settings/Controls/SettingAliasList.qml @@ -0,0 +1,155 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +ColumnLayout { + id: root + + required property string name + required property var object + required property string setting + + function addAlias() { + const list = [...root.object[root.setting]]; + list.push({ + from: "", + to: "" + }); + root.object[root.setting] = list; + Config.save(); + } + + function removeAlias(index) { + const list = [...root.object[root.setting]]; + list.splice(index, 1); + root.object[root.setting] = list; + Config.save(); + } + + function updateAlias(index, key, value) { + const list = [...root.object[root.setting]]; + const entry = [...list[index]]; + entry[key] = value; + list[index] = entry; + root.object[root.setting] = list; + Config.save(); + } + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + Repeater { + model: [...root.object[root.setting]] + + Item { + required property int index + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: layout.implicitHeight + Appearance.padding.smaller * 2 + + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: -(Appearance.spacing.smaller / 2) + color: DynamicColors.tPalette.m3outlineVariant + implicitHeight: 1 + visible: index !== 0 + } + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("From") + } + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: 33 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.from ?? "" + + onEditingFinished: root.updateAlias(index, "from", text) + } + } + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "delete" + type: IconButton.Tonal + + onClicked: root.removeAlias(index) + } + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("To") + } + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: 33 + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.to ?? "" + + onEditingFinished: root.updateAlias(index, "to", text) + } + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: root.addAlias() + } + + CustomText { + Layout.fillWidth: true + text: qsTr("Add alias") + } + } +} diff --git a/Modules/Settings/Controls/SettingBarEntryList.qml b/Modules/Settings/Controls/SettingBarEntryList.qml new file mode 100644 index 0000000..f082133 --- /dev/null +++ b/Modules/Settings/Controls/SettingBarEntryList.qml @@ -0,0 +1,523 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import QtQuick.Layouts +import QtQml.Models +import qs.Components +import qs.Config + +Item { + id: root + + property bool dragActive: false + property real dragHeight: 0 + property real dragStartX: 0 + property real dragStartY: 0 + property real dragX: 0 + property real dragY: 0 + property var draggedModelData: null + property string draggedUid: "" + property bool dropAnimating: false + required property string name + required property var object + property var pendingCommitEntries: [] + required property string setting + property int uidCounter: 0 + property var visualEntries: [] + + function beginVisualDrag(uid, modelData, item) { + const pos = item.mapToItem(root, 0, 0); + + root.draggedUid = uid; + root.draggedModelData = modelData; + root.dragHeight = item.height; + root.dragStartX = pos.x; + root.dragStartY = pos.y; + root.dragX = pos.x; + root.dragY = pos.y; + root.dragActive = true; + root.dropAnimating = false; + root.pendingCommitEntries = []; + } + + function commitVisualOrder(entries) { + const list = []; + + for (let i = 0; i < entries.length; i++) + list.push(entries[i].entry); + + root.object[root.setting] = list; + Config.save(); + root.rebuildVisualEntries(); + } + + function endVisualDrag() { + const entries = root.visualEntries.slice(); + const finalIndex = root.indexForUid(root.draggedUid); + const finalItem = listView.itemAtIndex(finalIndex); + + root.dragActive = false; + + if (!finalItem) { + root.pendingCommitEntries = entries; + root.finishVisualDrag(); + return; + } + + const pos = finalItem.mapToItem(root, 0, 0); + + root.pendingCommitEntries = entries; + root.dropAnimating = true; + settleX.to = pos.x; + settleY.to = pos.y; + settleAnim.start(); + } + + function ensureVisualEntries() { + if (!root.dragActive && !root.dropAnimating) + root.rebuildVisualEntries(); + } + + function finishVisualDrag() { + const entries = root.pendingCommitEntries.slice(); + + root.dragActive = false; + root.dropAnimating = false; + root.draggedUid = ""; + root.draggedModelData = null; + root.pendingCommitEntries = []; + root.dragHeight = 0; + + root.commitVisualOrder(entries); + } + + function iconForId(id) { + switch (id) { + case "workspaces": + return "dashboard"; + case "audio": + return "volume_up"; + case "media": + return "play_arrow"; + case "resources": + return "monitoring"; + case "updates": + return "system_update"; + case "dash": + return "space_dashboard"; + case "spacer": + return "horizontal_rule"; + case "activeWindow": + return "web_asset"; + case "tray": + return "widgets"; + case "upower": + return "battery_full"; + case "network": + return "wifi"; + case "clock": + return "schedule"; + case "notifBell": + return "notifications"; + default: + return "drag_indicator"; + } + } + + function indexForUid(uid) { + for (let i = 0; i < root.visualEntries.length; i++) { + if (root.visualEntries[i].uid === uid) + return i; + } + + return -1; + } + + function labelForId(id) { + switch (id) { + case "workspaces": + return qsTr("Workspaces"); + case "audio": + return qsTr("Audio"); + case "media": + return qsTr("Media"); + case "resources": + return qsTr("Resources"); + case "updates": + return qsTr("Updates"); + case "dash": + return qsTr("Dash"); + case "spacer": + return qsTr("Spacer"); + case "activeWindow": + return qsTr("Active window"); + case "tray": + return qsTr("Tray"); + case "upower": + return qsTr("Power"); + case "network": + return qsTr("Network"); + case "clock": + return qsTr("Clock"); + case "notifBell": + return qsTr("Notification bell"); + default: + return id; + } + } + + function moveArrayItem(list, from, to) { + const next = list.slice(); + const [item] = next.splice(from, 1); + next.splice(to, 0, item); + return next; + } + + function previewVisualMove(from, hovered, before) { + let to = hovered + (before ? 0 : 1); + + if (to > from) + to -= 1; + + to = Math.max(0, Math.min(visualModel.items.count - 1, to)); + + if (from === to) + return; + + visualModel.items.move(from, to); + root.visualEntries = root.moveArrayItem(root.visualEntries, from, to); + } + + function rebuildVisualEntries() { + const entries = root.object[root.setting] ?? []; + const next = []; + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + let existing = null; + + for (let j = 0; j < root.visualEntries.length; j++) { + if (root.visualEntries[j].entry === entry) { + existing = root.visualEntries[j]; + break; + } + } + + if (existing) + next.push(existing); + else + next.push({ + uid: `entry-${root.uidCounter++}`, + entry + }); + } + + root.visualEntries = next; + } + + function updateEntry(index, value) { + const list = [...root.object[root.setting]]; + const entry = list[index]; + entry.enabled = value; + list[index] = entry; + root.object[root.setting] = list; + Config.save(); + root.ensureVisualEntries(); + } + + Layout.fillWidth: true + implicitHeight: layout.implicitHeight + + Component.onCompleted: root.rebuildVisualEntries() + + ParallelAnimation { + id: settleAnim + + onFinished: root.finishVisualDrag() + + Anim { + id: settleX + + duration: Appearance.anim.durations.normal + property: "dragX" + target: root + } + + Anim { + id: settleY + + duration: Appearance.anim.durations.normal + property: "dragY" + target: root + } + } + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Appearance.spacing.smaller + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + DelegateModel { + id: visualModel + + delegate: entryDelegate + + model: ScriptModel { + objectProp: "uid" + values: root.visualEntries + } + } + + ListView { + id: listView + + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + boundsBehavior: Flickable.StopAtBounds + clip: false + implicitHeight: contentHeight + implicitWidth: width + interactive: !(root.dragActive || root.dropAnimating) + model: visualModel + spacing: Appearance.spacing.small + + add: Transition { + Anim { + properties: "opacity,scale" + to: 1 + } + } + addDisplaced: Transition { + Anim { + duration: Appearance.anim.durations.normal + property: "y" + } + } + displaced: Transition { + Anim { + duration: Appearance.anim.durations.normal + property: "y" + } + } + move: Transition { + Anim { + duration: Appearance.anim.durations.normal + property: "y" + } + } + removeDisplaced: Transition { + Anim { + duration: Appearance.anim.durations.normal + property: "y" + } + } + } + } + + Loader { + active: root.dragActive || root.dropAnimating + asynchronous: false + + sourceComponent: Item { + Drag.active: root.dragActive + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + height: proxyRect.implicitHeight + implicitHeight: proxyRect.implicitHeight + implicitWidth: listView.width + visible: root.draggedModelData !== null + width: listView.width + x: root.dragX + y: root.dragY + z: 100 + + Drag.source: QtObject { + property string uid: root.draggedUid + property int visualIndex: root.indexForUid(root.draggedUid) + } + + CustomRect { + id: proxyRect + + color: DynamicColors.tPalette.m3surface + implicitHeight: proxyRow.implicitHeight + Appearance.padding.small * 2 + implicitWidth: parent.width + opacity: 0.95 + radius: Appearance.rounding.normal + width: parent.width + + RowLayout { + id: proxyRow + + anchors.fill: parent + anchors.margins: Appearance.padding.small + spacing: Appearance.spacing.normal + + CustomRect { + color: Qt.alpha(DynamicColors.palette.m3onSurface, 0.12) + implicitHeight: 32 + implicitWidth: implicitHeight + radius: Appearance.rounding.small + + MaterialIcon { + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurfaceVariant + text: "drag_indicator" + } + } + + MaterialIcon { + color: DynamicColors.palette.m3onSurfaceVariant + text: root.iconForId(root.draggedModelData?.entry?.id ?? "") + } + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.labelForId(root.draggedModelData?.entry?.id ?? "") + } + + CustomSwitch { + checked: root.draggedModelData?.entry?.enabled ?? true + enabled: false + } + } + } + } + } + + Component { + id: entryDelegate + + DropArea { + id: slot + + readonly property var entryData: modelData.entry + required property var modelData + readonly property string uid: modelData.uid + + function previewReorder(drag) { + const source = drag.source; + if (!source || !source.uid || source.uid === slot.uid) + return; + + const from = source.visualIndex; + const hovered = slot.DelegateModel.itemsIndex; + + if (from < 0 || hovered < 0) + return; + + root.previewVisualMove(from, hovered, drag.y < height / 2); + } + + height: entryRow.implicitHeight + implicitHeight: entryRow.implicitHeight + implicitWidth: listView.width + width: ListView.view ? ListView.view.width : listView.width + + onEntered: drag => previewReorder(drag) + onPositionChanged: drag => previewReorder(drag) + + CustomRect { + id: entryRow + + anchors.fill: parent + color: DynamicColors.tPalette.m3surface + implicitHeight: entryLayout.implicitHeight + Appearance.padding.small * 2 + implicitWidth: parent.width + opacity: root.draggedUid === slot.uid ? 0 : 1 + radius: Appearance.rounding.full + + Behavior on opacity { + Anim { + } + } + + RowLayout { + id: entryLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.small + spacing: Appearance.spacing.normal + + CustomRect { + id: handle + + color: Qt.alpha(DynamicColors.palette.m3onSurface, handleDrag.active ? 0.12 : handleHover.hovered ? 0.09 : 0.06) + implicitHeight: 32 + implicitWidth: implicitHeight + radius: Appearance.rounding.full + + Behavior on color { + CAnim { + } + } + + MaterialIcon { + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurfaceVariant + text: "drag_indicator" + } + + HoverHandler { + id: handleHover + + cursorShape: handleDrag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor + } + + DragHandler { + id: handleDrag + + enabled: true + grabPermissions: PointerHandler.CanTakeOverFromAnything + target: null + xAxis.enabled: false + yAxis.enabled: true + + onActiveChanged: { + if (active) { + root.beginVisualDrag(slot.uid, slot.modelData, entryRow); + } else if (root.draggedUid === slot.uid) { + root.endVisualDrag(); + } + } + onActiveTranslationChanged: { + if (!active || root.draggedUid !== slot.uid) + return; + + root.dragX = root.dragStartX; + root.dragY = root.dragStartY + activeTranslation.y; + } + } + } + + MaterialIcon { + color: DynamicColors.palette.m3onSurfaceVariant + text: root.iconForId(slot.entryData.id) + } + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.labelForId(slot.entryData.id) + } + + CustomSwitch { + Layout.rightMargin: Appearance.padding.small + checked: slot.entryData.enabled ?? true + + onToggled: root.updateEntry(slot.DelegateModel.itemsIndex, checked) + } + } + } + } + } +} diff --git a/Modules/Settings/Controls/SettingInput.qml b/Modules/Settings/Controls/SettingInput.qml index 58ffe24..1421664 100644 --- a/Modules/Settings/Controls/SettingInput.qml +++ b/Modules/Settings/Controls/SettingInput.qml @@ -13,7 +13,6 @@ Item { function formattedValue(): string { const value = root.object[root.setting]; - console.log(value); if (value === null || value === undefined) return ""; @@ -44,15 +43,16 @@ Item { id: rect Layout.preferredHeight: 33 - Layout.preferredWidth: Math.max(Math.min(textField.contentWidth + Appearance.padding.normal * 3, 200), 50) - color: DynamicColors.tPalette.m3surface - radius: Appearance.rounding.small + Layout.preferredWidth: Math.max(Math.min(textField.contentWidth + Appearance.padding.normal * 2, 550), 50) + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full CustomTextField { id: textField anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter + implicitWidth: Math.min(contentWidth, 550) text: root.formattedValue() onEditingFinished: { diff --git a/Modules/Settings/Controls/SettingList.qml b/Modules/Settings/Controls/SettingList.qml index 2a905ca..3d2dd66 100644 --- a/Modules/Settings/Controls/SettingList.qml +++ b/Modules/Settings/Controls/SettingList.qml @@ -236,8 +236,7 @@ Item { font.pointSize: Appearance.font.size.large icon: "add" - onClicked: console.log(button.width) - // onClicked: root.addActiveActionRequested() + onClicked: root.addActiveActionRequested() } CustomText { diff --git a/Modules/Settings/Controls/SettingReadOnly.qml b/Modules/Settings/Controls/SettingReadOnly.qml new file mode 100644 index 0000000..1fe580a --- /dev/null +++ b/Modules/Settings/Controls/SettingReadOnly.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property string value + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomText { + color: DynamicColors.palette.m3onSurfaceVariant + font.family: Appearance.font.family.mono + font.pointSize: Appearance.font.size.normal + text: root.value + } + } +} diff --git a/Modules/Settings/Controls/SettingSpinBox.qml b/Modules/Settings/Controls/SettingSpinBox.qml new file mode 100644 index 0000000..0cbf240 --- /dev/null +++ b/Modules/Settings/Controls/SettingSpinBox.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property var object + required property string setting + property real max: Infinity + property real min: -Infinity + property real step: 1 + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + CustomText { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomSpinBox { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + max: root.max + min: root.min + step: root.step + value: Number(root.object[root.setting] ?? 0) + + onValueModified: function (value) { + root.object[root.setting] = value; + Config.save(); + } + } + } +} diff --git a/Modules/Settings/Controls/SettingSpinner.qml b/Modules/Settings/Controls/SettingSpinner.qml index 960e5ca..2663343 100644 --- a/Modules/Settings/Controls/SettingSpinner.qml +++ b/Modules/Settings/Controls/SettingSpinner.qml @@ -18,7 +18,6 @@ Item { function formattedValue(setting: string): string { const value = root.object[setting]; - console.log(value); if (value === null || value === undefined) return ""; diff --git a/Modules/Settings/Controls/SettingStringList.qml b/Modules/Settings/Controls/SettingStringList.qml new file mode 100644 index 0000000..c78414b --- /dev/null +++ b/Modules/Settings/Controls/SettingStringList.qml @@ -0,0 +1,41 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property var object + required property string setting + property string addLabel: qsTr("Add entry") + + Layout.fillWidth: true + Layout.preferredHeight: layout.implicitHeight + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + StringListEditor { + Layout.fillWidth: true + addLabel: root.addLabel + values: [...(root.object[root.setting] ?? [])] + + onListEdited: function (values) { + root.object[root.setting] = values; + Config.save(); + } + } + } +} diff --git a/Modules/Settings/Controls/SettingsHeader.qml b/Modules/Settings/Controls/SettingsHeader.qml new file mode 100644 index 0000000..257757a --- /dev/null +++ b/Modules/Settings/Controls/SettingsHeader.qml @@ -0,0 +1,21 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + id: root + + required property string name + + Layout.preferredHeight: 60 + Layout.preferredWidth: 200 + + CustomText { + anchors.fill: parent + font.bold: true + font.pointSize: Appearance.font.size.large * 2 + text: root.name + verticalAlignment: Text.AlignVCenter + } +} diff --git a/Modules/Settings/Controls/SettingsPage.qml b/Modules/Settings/Controls/SettingsPage.qml new file mode 100644 index 0000000..1cfefec --- /dev/null +++ b/Modules/Settings/Controls/SettingsPage.qml @@ -0,0 +1,41 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config +import qs.Helpers + +CustomFlickable { + id: root + + default property alias contentData: clayout.data + + contentHeight: clayout.implicitHeight + + TapHandler { + acceptedButtons: Qt.LeftButton + + onTapped: function (eventPoint) { + const menu = SettingsDropdowns.activeMenu; + if (!menu) + return; + + const p = eventPoint.scenePosition; + + if (SettingsDropdowns.hit(SettingsDropdowns.activeTrigger, p)) + return; + + if (SettingsDropdowns.hit(menu, p)) + return; + + SettingsDropdowns.closeActive(); + } + } + + ColumnLayout { + id: clayout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + } +} diff --git a/Modules/Settings/Controls/SettingsSection.qml b/Modules/Settings/Controls/SettingsSection.qml new file mode 100644 index 0000000..081447a --- /dev/null +++ b/Modules/Settings/Controls/SettingsSection.qml @@ -0,0 +1,26 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + id: root + + default property alias contentData: layout.data + property real contentPadding: Appearance.padding.large + + Layout.fillWidth: true + Layout.preferredHeight: layout.implicitHeight + contentPadding * 2 + color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal - Appearance.padding.smaller + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.margins: root.contentPadding + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.normal + } +} diff --git a/Modules/Settings/Controls/StringListEditor.qml b/Modules/Settings/Controls/StringListEditor.qml new file mode 100644 index 0000000..701e334 --- /dev/null +++ b/Modules/Settings/Controls/StringListEditor.qml @@ -0,0 +1,107 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +ColumnLayout { + id: root + + property string addLabel: qsTr("Add entry") + property var values: [] + + signal listEdited(var values) + + function addValue() { + const list = [...root.values]; + list.push(""); + root.listEdited(list); + } + + function removeValue(index) { + const list = [...root.values]; + list.splice(index, 1); + root.listEdited(list); + } + + function updateValue(index, value) { + const list = [...root.values]; + list[index] = value; + root.listEdited(list); + } + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + Repeater { + model: [...root.values] + + Item { + required property int index + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: -(Appearance.spacing.smaller / 2) + color: DynamicColors.tPalette.m3outlineVariant + implicitHeight: 1 + visible: index !== 0 + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: 33 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: String(modelData ?? "") + + onEditingFinished: root.updateValue(index, text) + } + } + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "delete" + type: IconButton.Tonal + + onClicked: root.removeValue(index) + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: root.addValue() + } + + CustomText { + Layout.fillWidth: true + text: root.addLabel + } + } +} diff --git a/Modules/TrayItem.qml b/Modules/TrayItem.qml index 9580969..50fbf62 100644 --- a/Modules/TrayItem.qml +++ b/Modules/TrayItem.qml @@ -28,9 +28,10 @@ Item { root.popouts.currentName = `traymenu${root.ind}`; root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x); root.popouts.hasCurrent = true; - if (visibilities.sidebar || visibilities.dashboard) { + if (visibilities.sidebar || visibilities.dashboard || visibilities.settings) { visibilities.sidebar = false; visibilities.dashboard = false; + visibilities.settings = false; } } } -- 2.47.3 From ca49b9c6f170c54cfda8005e87ce0fb2a785d77d Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 16 Mar 2026 15:45:37 +0100 Subject: [PATCH 36/47] plans --- plans/ideas.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/plans/ideas.md b/plans/ideas.md index c9c51e2..8c23252 100644 --- a/plans/ideas.md +++ b/plans/ideas.md @@ -24,17 +24,23 @@ # Issues in settingsWindow (16-03-2026) -- [ ] Drawing tool falls behind when accelerating with the cursor (slow start -> faster movement). -- [ ] Undo option for Drawing tool? -- [ ] Size 1-45 kinda weird numbers (not a real issue = ragebait). -- [ ] Dock has an invisible border it has a visual that it attaches to; perhaps make it visible when the dock shows? -- [ ] Dock apps are clickable and navigates to app (good). If two instances are available, this feels arbitrarily chosen on one instance (maybe defaults to workspace closest to 1?) (like a selection or hover to see options). -- [ ] Dock cannot be closed with escape, user needs to click to leave Dock (Dock stops user from interacting with other apps like typing). +- [ ] Drawing tool falls behind when accelerating with the cursor (slow start -> faster movement). // Unfortunately this is a limitation of either Qt or the math in my methods, you are free to look through and see if you can come up with better and more performant calculations +- [ ] Dock has an invisible border it has a visual that it attaches to; perhaps make it visible when the dock shows? // Yes +- [ ] Dock apps are clickable and navigates to app (good). If two instances are available, this feels arbitrarily chosen on one instance (maybe defaults to workspace closest to 1?) (like a selection or hover to see options). // I intend to add popups on hover that show a preview of the opened windows, so you can select which one to focus +- [ ] Dock cannot be closed with escape, user needs to click to leave Dock (Dock stops user from interacting with other apps like typing). // Intentional. It uses HyprlandFocusGrab for managing shown/hidden states. - [ ] Global shortcut for opening Dock and perhaps keyboard navigation? (sounds hard to pull of) - [ ] If nc or osd global shortcut are used, bar is 100% transparent apart from modules, seems to ignore the regular hover state opacity. -- [ ] Should volume/pipewire module be hover as well? No other bar module is hover apart from the Dock (which is a hidden module activated by hover)? -- [ ] Calendar swipe to other month has no animation -> on purpose? +- [ ] Should volume/pipewire module be hover as well? No other bar module is hover apart from the Dock (which is a hidden module activated by hover)? // Unsure, probably + +- [x] Undo option for Drawing tool? // You can clear on right-click. True undo + would require me to store pen strokes in an array and is too advanced for a + simple drawing tool +- [x] Size 1-45 kinda weird numbers (not a real issue = ragebait). // It's just + the pixel width of the pencil. +- [x] Calendar swipe to other month has no animation -> on purpose? // On + purpose, QT doesn't allow for animations in their calendar grid. I used to have + an animation but it was extremely inefficient performance-wise. ## Additional questions -- [ ] Can some features be disabled? As in, will they be unloaded from RAM or how is it loaded in memory? Let's say I do not want to use the Dock and Drawing Tool and want to disable them, are they loaded in memory at all? Or all the called upon when the shortcut is hit? +- [x] Can some features be disabled? As in, will they be unloaded from RAM or how is it loaded in memory? Let's say I do not want to use the Dock and Drawing Tool and want to disable them, are they loaded in memory at all? Or all the called upon when the shortcut is hit? // None of the modules that are not shown on start are loaded into memory save for notifications. I will make the options to disable different parts functional at some point, but it wouldn't help memory usage since they are loaded on-demand already -- 2.47.3 From 46a1af82f54fd7eda280d131f4bfbe50297a3623 Mon Sep 17 00:00:00 2001 From: Aram Markarov Date: Mon, 16 Mar 2026 16:01:03 +0100 Subject: [PATCH 37/47] README.md update to match new config location. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f461a2b..a6d7e3b 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Below a full example of what it could look like. } ``` -Now you can add z-bar-qt as a nixpkg in environment.systemPackages (or optionally in your homePackages). +Now you can add z-bar-qt as a nixpkg in environment.systemPackages (or optionally in your homePackages). It is named zshell and for cli options; zshell-cli. ```nix { pkgs, inputs, ... }: @@ -108,7 +108,7 @@ You can now run `zshell` to run the bar. `zshell-cli` can be used for additional ## Configuration -Configuration is stored in `~/.config/z-bar/config.json`. Options include: +Configuration is stored in `~/.config/zshell/config.json`. Options include: | Option | Description | | :-------------------------------: | :---------------------------------------------------------: | -- 2.47.3 From f4c5ce08d227728700eee812883b09919448dfb8 Mon Sep 17 00:00:00 2001 From: AramMarkarov Date: Tue, 17 Mar 2026 04:20:25 +0100 Subject: [PATCH 38/47] preparation brightnesscontrol ddcutil and brightnessctl : missing osd functionality --- nix/default.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/default.nix b/nix/default.nix index 83edc51..636e6be 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -25,6 +25,8 @@ pkg-config, pythonEnv, zshell-cli, + ddcutil, + brightnessctl, }: let version = "1.0.0"; @@ -39,6 +41,8 @@ let bash hyprland zshell-cli + ddcutil + brightnessctl ]; fontconfig = makeFontsConf { -- 2.47.3 From 152b363da2794a31959db9c94191987960763b4d Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Tue, 17 Mar 2026 18:45:08 +0100 Subject: [PATCH 39/47] updates popout --- Config/AppearanceConf.qml | 1 + Drawers/Backgrounds.qml | 3 +- Helpers/Updates.qml | 74 +++++ Modules/Background.qml | 2 +- Modules/Bar/Bar.qml | 27 +- Modules/Content.qml | 9 + Modules/TrayMenu.qml | 392 ------------------------ Modules/Updates.qml | 37 --- Modules/Updates/UpdatesPopout.qml | 130 ++++++++ Modules/{ => Updates}/UpdatesWidget.qml | 1 + 10 files changed, 234 insertions(+), 442 deletions(-) create mode 100644 Helpers/Updates.qml delete mode 100644 Modules/TrayMenu.qml delete mode 100644 Modules/Updates.qml create mode 100644 Modules/Updates/UpdatesPopout.qml rename Modules/{ => Updates}/UpdatesWidget.qml (97%) diff --git a/Config/AppearanceConf.qml b/Config/AppearanceConf.qml index 93defb5..60c648c 100644 --- a/Config/AppearanceConf.qml +++ b/Config/AppearanceConf.qml @@ -79,6 +79,7 @@ JsonObject { property int normal: 17 * scale property real scale: 1 property int small: 12 * scale + property int smallest: 8 * scale } component Spacing: JsonObject { property int large: 20 * scale diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index 8c1fcef..5def5b9 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -48,7 +48,8 @@ Shape { Modules.Background { invertBottomRounding: wrapper.x <= 0 - startX: wrapper.x - 8 + rounding: root.panels.popouts.currentName.startsWith("updates") ? Appearance.rounding.normal : Appearance.rounding.smallest + startX: wrapper.x - rounding startY: wrapper.y wrapper: root.panels.popouts } diff --git a/Helpers/Updates.qml b/Helpers/Updates.qml new file mode 100644 index 0000000..fc4b517 --- /dev/null +++ b/Helpers/Updates.qml @@ -0,0 +1,74 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property int availableUpdates: 0 + property double now: Date.now() + property var updates: ({}) + + function formatUpdateTime(timestamp) { + const diffMs = root.now - timestamp; + const minuteMs = 60 * 1000; + const hourMs = 60 * minuteMs; + const dayMs = 24 * hourMs; + + if (diffMs < minuteMs) + return "just now"; + + if (diffMs < hourMs) + return Math.floor(diffMs / minuteMs) + " min ago"; + + if (diffMs < 48 * hourMs) + return Math.floor(diffMs / hourMs) + " hr ago"; + + return Qt.formatDateTime(new Date(timestamp), "dd hh:mm"); + } + + Timer { + interval: 1 + repeat: true + running: true + + onTriggered: { + updatesProc.running = true; + interval = 5000; + } + } + + Timer { + interval: 60000 + repeat: true + running: true + + onTriggered: root.now = Date.now() + } + + Process { + id: updatesProc + + command: ["checkupdates"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const output = this.text; + const lines = output.trim().split("\n").filter(line => line.length > 0); + + const oldMap = root.updates; + const now = Date.now(); + + root.updates = lines.reduce((acc, pkg) => { + acc[pkg] = oldMap[pkg] ?? now; + return acc; + }, {}); + root.availableUpdates = lines.length; + } + } + } +} diff --git a/Modules/Background.qml b/Modules/Background.qml index 483ce94..a726d17 100644 --- a/Modules/Background.qml +++ b/Modules/Background.qml @@ -9,7 +9,7 @@ ShapePath { readonly property bool flatten: wrapper.height < rounding * 2 property real ibr: invertBottomRounding ? -1 : 1 required property bool invertBottomRounding - readonly property real rounding: 8 + property real rounding: Appearance.rounding.smallest readonly property real roundingY: flatten ? wrapper.height / 2 : rounding required property Wrapper wrapper diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 72dfbb8..c801947 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -4,16 +4,17 @@ import Quickshell import QtQuick import QtQuick.Layouts import qs.Components -import qs.Modules as Bar +import qs.Modules import qs.Config import qs.Helpers import qs.Modules.UPower import qs.Modules.Network +import qs.Modules.Updates RowLayout { id: root - required property Bar.Wrapper popouts + required property Wrapper popouts required property ShellScreen screen readonly property int vPadding: 6 required property PersistentProperties visibilities @@ -47,6 +48,10 @@ RowLayout { popouts.currentName = "upower"; popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); popouts.hasCurrent = true; + } else if (id === "updates") { + popouts.currentName = "updates"; + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.hasCurrent = true; } } @@ -73,7 +78,7 @@ RowLayout { roleValue: "workspaces" delegate: WrappedLoader { - sourceComponent: Bar.Workspaces { + sourceComponent: Workspaces { screen: root.screen } } @@ -83,7 +88,7 @@ RowLayout { roleValue: "audio" delegate: WrappedLoader { - sourceComponent: Bar.AudioWidget { + sourceComponent: AudioWidget { } } } @@ -92,7 +97,7 @@ RowLayout { roleValue: "tray" delegate: WrappedLoader { - sourceComponent: Bar.TrayWidget { + sourceComponent: TrayWidget { loader: root popouts: root.popouts } @@ -103,7 +108,7 @@ RowLayout { roleValue: "resources" delegate: WrappedLoader { - sourceComponent: Bar.Resources { + sourceComponent: Resources { visibilities: root.visibilities } } @@ -113,7 +118,7 @@ RowLayout { roleValue: "updates" delegate: WrappedLoader { - sourceComponent: Bar.UpdatesWidget { + sourceComponent: UpdatesWidget { } } } @@ -122,7 +127,7 @@ RowLayout { roleValue: "notifBell" delegate: WrappedLoader { - sourceComponent: Bar.NotifBell { + sourceComponent: NotifBell { popouts: root.popouts visibilities: root.visibilities } @@ -133,7 +138,7 @@ RowLayout { roleValue: "clock" delegate: WrappedLoader { - sourceComponent: Bar.Clock { + sourceComponent: Clock { loader: root popouts: root.popouts visibilities: root.visibilities @@ -145,7 +150,7 @@ RowLayout { roleValue: "activeWindow" delegate: WrappedLoader { - sourceComponent: Bar.WindowTitle { + sourceComponent: WindowTitle { bar: root } } @@ -173,7 +178,7 @@ RowLayout { roleValue: "media" delegate: WrappedLoader { - sourceComponent: Bar.MediaWidget { + sourceComponent: MediaWidget { } } } diff --git a/Modules/Content.qml b/Modules/Content.qml index 54a1f45..0040b7a 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -8,6 +8,7 @@ import qs.Components import qs.Modules.WSOverview import qs.Modules.Network import qs.Modules.UPower +import qs.Modules.Updates Item { id: root @@ -92,6 +93,14 @@ Item { wrapper: root.wrapper } } + + Popout { + name: "updates" + + sourceComponent: UpdatesPopout { + wrapper: root.wrapper + } + } } component Popout: Loader { diff --git a/Modules/TrayMenu.qml b/Modules/TrayMenu.qml deleted file mode 100644 index f10ed9c..0000000 --- a/Modules/TrayMenu.qml +++ /dev/null @@ -1,392 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import QtQuick -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell.Hyprland -import QtQml -import qs.Effects -import qs.Config - -PanelWindow { - id: root - - property color backgroundColor: DynamicColors.tPalette.m3surface - required property PanelWindow bar - property int biggestWidth: 0 - property color disabledHighlightColor: DynamicColors.layer(DynamicColors.palette.m3primaryContainer, 0) - property color disabledTextColor: DynamicColors.layer(DynamicColors.palette.m3onSurface, 0) - property int entryHeight: 30 - property alias focusGrab: grab.active - property color highlightColor: DynamicColors.tPalette.m3primaryContainer - property int menuItemCount: menuOpener.children.values.length - property var menuStack: [] - property real scaleValue: 0 - property color textColor: DynamicColors.palette.m3onSurface - required property point trayItemRect - required property QsMenuHandle trayMenu - - signal finishedLoading - signal menuActionTriggered - - function goBack() { - if (root.menuStack.length > 0) { - menuChangeAnimation.start(); - root.biggestWidth = 0; - root.trayMenu = root.menuStack.pop(); - listLayout.positionViewAtBeginning(); - backEntry.visible = false; - } - } - - function updateMask() { - root.mask.changed(); - } - - color: "transparent" - - // onTrayMenuChanged: { - // listLayout.forceLayout(); - // } - - visible: false - - mask: Region { - id: mask - - item: menuRect - } - - onMenuActionTriggered: { - if (root.menuStack.length > 0) { - backEntry.visible = true; - } - } - onVisibleChanged: { - if (!visible) - root.menuStack.pop(); - backEntry.visible = false; - - openAnim.start(); - } - - QsMenuOpener { - id: menuOpener - - menu: root.trayMenu - } - - anchors { - bottom: true - left: true - right: true - top: true - } - - HyprlandFocusGrab { - id: grab - - active: false - windows: [root] - - onCleared: { - closeAnim.start(); - } - } - - SequentialAnimation { - id: menuChangeAnimation - - ParallelAnimation { - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "x" - target: translateAnim - to: -listLayout.width / 2 - } - - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.standard - from: 1 - property: "opacity" - target: columnLayout - to: 0 - } - } - - PropertyAction { - property: "menu" - target: columnLayout - } - - ParallelAnimation { - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.standard - from: 0 - property: "opacity" - target: columnLayout - to: 1 - } - - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.expressiveEffects - from: listLayout.width / 2 - property: "x" - target: translateAnim - to: 0 - } - } - } - - ParallelAnimation { - id: closeAnim - - onFinished: { - root.visible = false; - } - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - property: "implicitHeight" - target: menuRect - to: 0 - } - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 1 - property: "opacity" - targets: [menuRect, shadowRect] - to: 0 - } - } - - ParallelAnimation { - id: openAnim - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "implicitHeight" - target: menuRect - to: listLayout.contentHeight + (root.menuStack.length > 0 ? root.entryHeight + 10 : 10) - } - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "opacity" - targets: [menuRect, shadowRect] - to: 1 - } - } - - ShadowRect { - id: shadowRect - - anchors.fill: menuRect - radius: menuRect.radius - } - - Rectangle { - id: menuRect - - clip: true - color: root.backgroundColor - implicitHeight: listLayout.contentHeight + (root.menuStack.length > 0 ? root.entryHeight + 10 : 10) - implicitWidth: listLayout.contentWidth + 10 - radius: 8 - x: Math.round(root.trayItemRect.x - (menuRect.implicitWidth / 2) + 11) - y: Math.round(root.trayItemRect.y - 5) - - Behavior on implicitHeight { - NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - Behavior on implicitWidth { - NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - - ColumnLayout { - id: columnLayout - - anchors.fill: parent - anchors.margins: 5 - spacing: 0 - - transform: [ - Translate { - id: translateAnim - - x: 0 - y: 0 - } - ] - - ListView { - id: listLayout - - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - contentHeight: contentItem.childrenRect.height - contentWidth: root.biggestWidth - model: menuOpener.children - spacing: 0 - - delegate: Rectangle { - id: menuItem - - property var child: QsMenuOpener { - menu: menuItem.modelData - } - property bool containsMouseAndEnabled: mouseArea.containsMouse && menuItem.modelData.enabled - property bool containsMouseAndNotEnabled: mouseArea.containsMouse && !menuItem.modelData.enabled - required property int index - required property QsMenuEntry modelData - - anchors.left: parent.left - anchors.right: parent.right - color: menuItem.modelData.isSeparator ? "#20FFFFFF" : containsMouseAndEnabled ? root.highlightColor : containsMouseAndNotEnabled ? root.disabledHighlightColor : "transparent" - height: menuItem.modelData.isSeparator ? 1 : root.entryHeight - radius: 4 - visible: true - width: widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20 - - Behavior on color { - CAnim { - duration: 150 - } - } - - Component.onCompleted: { - var biggestWidth = root.biggestWidth; - var currentWidth = widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20; - if (currentWidth > biggestWidth) { - root.biggestWidth = currentWidth; - } - } - - TextMetrics { - id: widthMetrics - - text: menuItem.modelData.text - } - - MouseArea { - id: mouseArea - - acceptedButtons: Qt.LeftButton - anchors.fill: parent - hoverEnabled: true - preventStealing: true - propagateComposedEvents: true - - onClicked: { - if (!menuItem.modelData.hasChildren) { - if (menuItem.modelData.enabled) { - menuItem.modelData.triggered(); - closeAnim.start(); - } - } else { - root.menuStack.push(root.trayMenu); - menuChangeAnimation.start(); - root.biggestWidth = 0; - root.trayMenu = menuItem.modelData; - listLayout.positionViewAtBeginning(); - root.menuActionTriggered(); - } - } - } - - RowLayout { - anchors.fill: parent - - Text { - id: menuText - - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - Layout.leftMargin: 10 - color: menuItem.modelData.enabled ? root.textColor : root.disabledTextColor - text: menuItem.modelData.text - } - - Image { - id: iconImage - - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.maximumHeight: 20 - Layout.maximumWidth: 20 - Layout.rightMargin: 10 - fillMode: Image.PreserveAspectFit - layer.enabled: true - source: menuItem.modelData.icon - sourceSize.height: height - sourceSize.width: width - - layer.effect: ColorOverlay { - color: menuItem.modelData.enabled ? "white" : "gray" - } - } - - Text { - id: textArrow - - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.bottomMargin: 5 - Layout.maximumHeight: 20 - Layout.maximumWidth: 20 - Layout.rightMargin: 10 - color: menuItem.modelData.enabled ? "white" : "gray" - text: "" - visible: menuItem.modelData.hasChildren ?? false - } - } - } - } - - Rectangle { - id: backEntry - - Layout.fillWidth: true - Layout.preferredHeight: root.entryHeight - color: mouseAreaBack.containsMouse ? "#15FFFFFF" : "transparent" - radius: 4 - visible: false - - MouseArea { - id: mouseAreaBack - - anchors.fill: parent - hoverEnabled: true - - onClicked: { - root.goBack(); - } - } - - Text { - anchors.fill: parent - anchors.leftMargin: 10 - color: "white" - text: "Back " - verticalAlignment: Text.AlignVCenter - } - } - } - } -} diff --git a/Modules/Updates.qml b/Modules/Updates.qml deleted file mode 100644 index b84aeee..0000000 --- a/Modules/Updates.qml +++ /dev/null @@ -1,37 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Modules - -Singleton { - property int availableUpdates: 0 - - Timer { - interval: 1 - repeat: true - running: true - - onTriggered: { - updatesProc.running = true; - interval = 5000; - } - } - - Process { - id: updatesProc - - command: ["checkupdates"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - const output = this.text; - const lines = output.trim().split("\n").filter(line => line.length > 0); - availableUpdates = lines.length; - } - } - } -} diff --git a/Modules/Updates/UpdatesPopout.qml b/Modules/Updates/UpdatesPopout.qml new file mode 100644 index 0000000..11f5d9d --- /dev/null +++ b/Modules/Updates/UpdatesPopout.qml @@ -0,0 +1,130 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Config +import qs.Components +import qs.Modules +import qs.Helpers + +Item { + id: root + + required property var wrapper + + implicitHeight: profiles.implicitHeight + Appearance.padding.small + implicitWidth: profiles.implicitWidth + Appearance.padding.small * 2 + + CustomRect { + id: profiles + + anchors.horizontalCenter: parent.horizontalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: updatesList.contentHeight + Appearance.padding.small * 2 + implicitWidth: updatesList.contentWidth + Appearance.padding.small * 2 + radius: Appearance.rounding.small + + CustomListView { + id: updatesList + + anchors.centerIn: parent + contentHeight: childrenRect.height + contentWidth: 600 + implicitHeight: contentHeight + implicitWidth: contentWidth + spacing: Appearance.spacing.normal + + delegate: CustomRect { + id: update + + required property var modelData + readonly property list sections: modelData.update.split(" ") + + anchors.left: parent.left + anchors.right: parent.right + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: 50 + Appearance.padding.smaller * 2 + radius: Appearance.rounding.small - Appearance.padding.small + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + anchors.rightMargin: Appearance.padding.smaller + + MaterialIcon { + font.pointSize: Appearance.font.size.large * 2 + text: "package_2" + } + + ColumnLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + Layout.preferredHeight: 25 + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + text: update.sections[0] + } + + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3onSurfaceVariant + text: Updates.formatUpdateTime(update.modelData.timestamp) + } + } + + RowLayout { + Layout.fillHeight: true + Layout.preferredWidth: 300 + + CustomText { + id: versionFrom + + Layout.fillHeight: true + Layout.preferredWidth: 125 + color: DynamicColors.palette.m3tertiary + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + horizontalAlignment: Text.AlignHCenter + text: update.sections[1] + verticalAlignment: Text.AlignVCenter + } + + MaterialIcon { + Layout.fillHeight: true + color: DynamicColors.palette.m3secondary + font.pointSize: Appearance.font.size.extraLarge + horizontalAlignment: Text.AlignHCenter + text: "arrow_right_alt" + verticalAlignment: Text.AlignVCenter + } + + CustomText { + id: versionTo + + Layout.fillHeight: true + Layout.preferredWidth: 120 + color: DynamicColors.palette.m3primary + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + horizontalAlignment: Text.AlignHCenter + text: update.sections[3] + verticalAlignment: Text.AlignVCenter + } + } + } + } + model: ScriptModel { + id: script + + objectProp: "update" + values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({ + update, + timestamp + })) + } + } + } +} diff --git a/Modules/UpdatesWidget.qml b/Modules/Updates/UpdatesWidget.qml similarity index 97% rename from Modules/UpdatesWidget.qml rename to Modules/Updates/UpdatesWidget.qml index 36cdc67..0eaa63a 100644 --- a/Modules/UpdatesWidget.qml +++ b/Modules/Updates/UpdatesWidget.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import qs.Components import qs.Modules +import qs.Helpers import qs.Config CustomRect { -- 2.47.3 From 9e8ee9b9de5c9b050f1762306d36c7a8b2f69c3a Mon Sep 17 00:00:00 2001 From: Aram Markarov Date: Tue, 17 Mar 2026 21:26:36 +0100 Subject: [PATCH 40/47] updated ideas.md --- plans/ideas.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plans/ideas.md b/plans/ideas.md index 8c23252..b4df49b 100644 --- a/plans/ideas.md +++ b/plans/ideas.md @@ -1,6 +1,7 @@ # Ideas/Features - [ ] Change volume for `$BROWSER` environment variable? Most general media source apart from separate music/video players. +- [ ] Hyprsunset module. # Stupid idea's from Daivin -- 2.47.3 From 7a61cc428055ee7f24e9412d25401b2cddef91a4 Mon Sep 17 00:00:00 2001 From: Aram Markarov Date: Tue, 17 Mar 2026 21:28:11 +0100 Subject: [PATCH 41/47] warning in README.md for nix users --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f461a2b..2da6ded 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ This installs the QML plugin to `/usr/lib/qt6/qml`. ### NixOS +**Note that not all features work well. This is due to limited testing on NixOS.** + In your flake.nix file, add the following in your inputs. ```nix -- 2.47.3 From c4a3206ffd4970b4569a54c6492f96bf45ae173f Mon Sep 17 00:00:00 2001 From: Aram Markarov Date: Tue, 17 Mar 2026 21:37:21 +0100 Subject: [PATCH 42/47] removing 'result' and adjusted .gitignore to not include result symlink folder --- .gitignore | 2 +- result | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 120000 result diff --git a/.gitignore b/.gitignore index 295910b..ad22547 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.result* +./result/ .pyre/ .cache/ .venv/ diff --git a/result b/result deleted file mode 120000 index 56ae149..0000000 --- a/result +++ /dev/null @@ -1 +0,0 @@ -/nix/store/dgf42nmv557p8lg0kv1g0cwjk8cnvalx-zshell-1.0.0 \ No newline at end of file -- 2.47.3 From 159e10cc0f87faa263d024e203df617dd542310d Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Tue, 17 Mar 2026 23:59:06 +0100 Subject: [PATCH 43/47] updates popout --- Modules/Updates/UpdatesPopout.qml | 213 +++++++++++++++++------------- 1 file changed, 118 insertions(+), 95 deletions(-) diff --git a/Modules/Updates/UpdatesPopout.qml b/Modules/Updates/UpdatesPopout.qml index 11f5d9d..2b592d3 100644 --- a/Modules/Updates/UpdatesPopout.qml +++ b/Modules/Updates/UpdatesPopout.qml @@ -8,123 +8,146 @@ import qs.Components import qs.Modules import qs.Helpers -Item { +CustomClippingRect { id: root + readonly property int itemHeight: 50 + Appearance.padding.smaller * 2 required property var wrapper - implicitHeight: profiles.implicitHeight + Appearance.padding.small - implicitWidth: profiles.implicitWidth + Appearance.padding.small * 2 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: updatesList.visible ? updatesList.implicitHeight + Appearance.padding.small * 2 : noUpdates.height + implicitWidth: updatesList.visible ? updatesList.contentWidth + Appearance.padding.small * 2 : noUpdates.width + radius: Appearance.rounding.small - CustomRect { - id: profiles + Item { + id: noUpdates - anchors.horizontalCenter: parent.horizontalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: updatesList.contentHeight + Appearance.padding.small * 2 - implicitWidth: updatesList.contentWidth + Appearance.padding.small * 2 - radius: Appearance.rounding.small + anchors.centerIn: parent + height: 200 + visible: script.values.length === 0 + width: 300 - CustomListView { - id: updatesList + MaterialIcon { + id: noUpdatesIcon - anchors.centerIn: parent - contentHeight: childrenRect.height - contentWidth: 600 - implicitHeight: contentHeight - implicitWidth: contentWidth - spacing: Appearance.spacing.normal + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + color: DynamicColors.tPalette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.extraLarge * 3 + horizontalAlignment: Text.AlignHCenter + text: "check" + } - delegate: CustomRect { - id: update + CustomText { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: noUpdatesIcon.bottom + color: DynamicColors.tPalette.m3onSurfaceVariant + horizontalAlignment: Text.AlignHCenter + text: qsTr("No updates available") + verticalAlignment: Text.AlignVCenter + } + } - required property var modelData - readonly property list sections: modelData.update.split(" ") + CustomListView { + id: updatesList - anchors.left: parent.left - anchors.right: parent.right - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: 50 + Appearance.padding.smaller * 2 - radius: Appearance.rounding.small - Appearance.padding.small + anchors.centerIn: parent + contentHeight: childrenRect.height + contentWidth: 600 + implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing) + implicitWidth: contentWidth + spacing: Appearance.spacing.normal + visible: script.values.length > 0 + + delegate: CustomRect { + id: update + + required property var modelData + readonly property list sections: modelData.update.split(" ") + + anchors.left: parent.left + anchors.right: parent.right + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: root.itemHeight + radius: Appearance.rounding.small - Appearance.padding.small + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + anchors.rightMargin: Appearance.padding.smaller + + MaterialIcon { + font.pointSize: Appearance.font.size.large * 2 + text: "package_2" + } + + ColumnLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + Layout.preferredHeight: 25 + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + text: update.sections[0] + } + + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3onSurfaceVariant + text: Updates.formatUpdateTime(update.modelData.timestamp) + } + } RowLayout { - anchors.fill: parent - anchors.leftMargin: Appearance.padding.smaller - anchors.rightMargin: Appearance.padding.smaller + Layout.fillHeight: true + Layout.preferredWidth: 300 + + CustomText { + id: versionFrom + + Layout.fillHeight: true + Layout.preferredWidth: 125 + color: DynamicColors.palette.m3tertiary + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + horizontalAlignment: Text.AlignHCenter + text: update.sections[1] + verticalAlignment: Text.AlignVCenter + } MaterialIcon { - font.pointSize: Appearance.font.size.large * 2 - text: "package_2" - } - - ColumnLayout { - Layout.fillWidth: true - - CustomText { - Layout.fillWidth: true - Layout.preferredHeight: 25 - elide: Text.ElideRight - font.pointSize: Appearance.font.size.large - text: update.sections[0] - } - - CustomText { - Layout.fillWidth: true - color: DynamicColors.palette.m3onSurfaceVariant - text: Updates.formatUpdateTime(update.modelData.timestamp) - } - } - - RowLayout { Layout.fillHeight: true - Layout.preferredWidth: 300 + color: DynamicColors.palette.m3secondary + font.pointSize: Appearance.font.size.extraLarge + horizontalAlignment: Text.AlignHCenter + text: "arrow_right_alt" + verticalAlignment: Text.AlignVCenter + } - CustomText { - id: versionFrom + CustomText { + id: versionTo - Layout.fillHeight: true - Layout.preferredWidth: 125 - color: DynamicColors.palette.m3tertiary - elide: Text.ElideRight - font.pointSize: Appearance.font.size.large - horizontalAlignment: Text.AlignHCenter - text: update.sections[1] - verticalAlignment: Text.AlignVCenter - } - - MaterialIcon { - Layout.fillHeight: true - color: DynamicColors.palette.m3secondary - font.pointSize: Appearance.font.size.extraLarge - horizontalAlignment: Text.AlignHCenter - text: "arrow_right_alt" - verticalAlignment: Text.AlignVCenter - } - - CustomText { - id: versionTo - - Layout.fillHeight: true - Layout.preferredWidth: 120 - color: DynamicColors.palette.m3primary - elide: Text.ElideRight - font.pointSize: Appearance.font.size.large - horizontalAlignment: Text.AlignHCenter - text: update.sections[3] - verticalAlignment: Text.AlignVCenter - } + Layout.fillHeight: true + Layout.preferredWidth: 120 + color: DynamicColors.palette.m3primary + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + horizontalAlignment: Text.AlignHCenter + text: update.sections[3] + verticalAlignment: Text.AlignVCenter } } } - model: ScriptModel { - id: script + } + model: ScriptModel { + id: script - objectProp: "update" - values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({ - update, - timestamp - })) - } + objectProp: "update" + values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({ + update, + timestamp + })) } } } -- 2.47.3 From bc67da35e4a4e6cde0375b1ca37d6e722ba25f2a Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Wed, 18 Mar 2026 11:37:14 +0100 Subject: [PATCH 44/47] updates persistence --- Helpers/Updates.qml | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Helpers/Updates.qml b/Helpers/Updates.qml index fc4b517..7ad83b9 100644 --- a/Helpers/Updates.qml +++ b/Helpers/Updates.qml @@ -4,11 +4,13 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io +import qs.Paths Singleton { id: root property int availableUpdates: 0 + property bool loaded property double now: Date.now() property var updates: ({}) @@ -30,12 +32,23 @@ Singleton { return Qt.formatDateTime(new Date(timestamp), "dd hh:mm"); } + onUpdatesChanged: { + if (!root.loaded) + return; + + saveTimer.restart(); + availableUpdates = Object.keys(updates).length; + } + Timer { interval: 1 repeat: true running: true onTriggered: { + if (!root.loaded) + return; + updatesProc.running = true; interval = 5000; } @@ -71,4 +84,40 @@ Singleton { } } } + + Timer { + id: saveTimer + + interval: 1000 + + onTriggered: storage.setText(JSON.stringify(root.updates)) + } + + FileView { + id: storage + + path: `${Paths.state}/updates.json` + + onLoadFailed: err => { + if (err === FileViewError.FileNotFound) { + root.updates = ({}); + root.loaded = true; + setText("{}"); + return; + } + + root.updates = ({}); + root.loaded = true; + } + onLoaded: { + try { + const data = JSON.parse(text()); + root.updates = data && typeof data === "object" && !Array.isArray(data) ? data : {}; + } catch (e) { + root.updates = ({}); + } + + root.loaded = true; + } + } } -- 2.47.3 From 65703f3e71044c5726463f19902116b8b12fd226 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Wed, 18 Mar 2026 13:04:29 +0100 Subject: [PATCH 45/47] border color --- Drawers/Windows.qml | 3 ++- Modules/Bar/Border.qml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index be40c58..98772c7 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -129,7 +129,7 @@ Variants { Binding { property: "bar" target: visibilities - value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif || visibilities.resources + value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif || visibilities.resources || visibilities.settings || bar.isHovered when: Config.barConfig.autoHide } @@ -146,6 +146,7 @@ Variants { Border { bar: bar + visibilities: visibilities } Backgrounds { diff --git a/Modules/Bar/Border.qml b/Modules/Bar/Border.qml index dcf08af..d745767 100644 --- a/Modules/Bar/Border.qml +++ b/Modules/Bar/Border.qml @@ -11,12 +11,13 @@ Item { id: root required property Item bar + required property PersistentProperties visibilities anchors.fill: parent CustomRect { anchors.fill: parent - color: !root.bar.isHovered && Config.barConfig.autoHide ? "transparent" : DynamicColors.palette.m3surface + color: DynamicColors.palette.m3surface layer.enabled: true layer.effect: MultiEffect { -- 2.47.3 From d885037739c9e98e0856476aacb6960ad2fd3580 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Wed, 18 Mar 2026 13:17:11 +0100 Subject: [PATCH 46/47] border --- Modules/Bar/Border.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Bar/Border.qml b/Modules/Bar/Border.qml index d745767..ff9e476 100644 --- a/Modules/Bar/Border.qml +++ b/Modules/Bar/Border.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick import QtQuick.Effects -import qs.Modules import qs.Config import qs.Components @@ -17,6 +16,7 @@ Item { CustomRect { anchors.fill: parent + anchors.margins: -1 color: DynamicColors.palette.m3surface layer.enabled: true @@ -38,8 +38,8 @@ Item { Rectangle { anchors.fill: parent - anchors.margins: Config.barConfig.border - anchors.topMargin: root.bar.implicitHeight + anchors.margins: Config.barConfig.border + 1 + anchors.topMargin: root.bar.implicitHeight + 1 radius: Config.barConfig.border > 0 ? Config.barConfig.rounding : 0 topLeftRadius: Config.barConfig.rounding topRightRadius: Config.barConfig.rounding -- 2.47.3 From 9297fa4da2c9fda370dda2a6a9dd78253c7a0d79 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Wed, 18 Mar 2026 13:21:18 +0100 Subject: [PATCH 47/47] settings fix --- Modules/Bar/Bar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index c801947..df30e99 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -28,7 +28,7 @@ RowLayout { return; } - if (visibilities.sidebar || visibilities.dashboard || visibilities.resources) + if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings) return; const id = ch.id; -- 2.47.3