aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-07 12:38:18 +0100
committerDanilo M. <danix@danix.xyz>2026-11-07 12:38:18 +0100
commit3d045076d49ee7cc074f670806a823099ea9fb09 (patch)
tree32760f1d5075ad14adc67c182f0df883c9a13e53
parentaf008cbd9f5d8d03eff0aa8ac1c7856bb30412e0 (diff)
downloadquickshell-3d045076d49ee7cc074f670806a823099ea9fb09.tar.gz
quickshell-3d045076d49ee7cc074f670806a823099ea9fb09.zip
settings: one snackbar for every page's messages
Save, apply and upload results went to the page header, one line capped at 420px and cut with an ellipsis. They now go to a snackbar at the foot of the settings window, wrapped in full: green fades after 6s, yellow and red stay until dismissed or replaced, and changing page clears it. Snack.qml is the singleton pages call (ok, wait, error); Snackbar.qml draws it once in Settings.qml. SaveBar forwards its notice and error, so the 24 pages using it change nothing; the header keeps only the draft's state. The boot logo page calls Snack directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--desktop/README.md9
-rw-r--r--desktop/SaveBar.qml24
-rw-r--r--desktop/Settings.qml9
-rw-r--r--desktop/Snack.qml41
-rw-r--r--desktop/Snackbar.qml71
-rw-r--r--desktop/modules/sonix/BootlogoModule.qml2
-rw-r--r--desktop/modules/sonix/BootlogoTab.qml15
7 files changed, 146 insertions, 25 deletions
diff --git a/desktop/README.md b/desktop/README.md
index c7aea52..9e17e7a 100644
--- a/desktop/README.md
+++ b/desktop/README.md
@@ -56,6 +56,15 @@ page's draft lives in the page, so it is not marked in the sidebar and
leaving the page drops it. Git and autostart act a row at a time and the
theme applies on click, so they have no bar.
+What a save, an apply or an upload says goes to the snackbar at the foot of
+the window (`Snack.qml`, drawn by `Snackbar.qml`), in full and wrapped: green
+fades after 6s, yellow (waiting) and red (failed) stay until dismissed with
+its cross or replaced by the next message. Changing page or closing the
+window clears it. SaveBar forwards its `notice` and `error` there, so a page
+using it needs nothing more; any other page calls `Snack.ok(text)`,
+`Snack.wait(text)` or `Snack.error(text)`. The header keeps only the draft's
+state beside the buttons: "unsaved changes", or why Save is held back.
+
The sidebar's edge drags to resize it, between 160 and 420px; the width is
saved in the preferences file on release.
diff --git a/desktop/SaveBar.qml b/desktop/SaveBar.qml
index 147d00b..239b896 100644
--- a/desktop/SaveBar.qml
+++ b/desktop/SaveBar.qml
@@ -18,10 +18,11 @@ import QtQuick
// does (Apply and Reset for a staged wallpaper or cursor, Save + Restart for
// a daemon that rereads only at start); busy shows while a save runs.
//
-// The outcome of a save is reported here too, not at the foot of the page:
-// notice is what a finished save says ("saved"), shown once the page is
-// clean, in noticeColor; error is a save refused or failed, which leaves the
-// draft dirty, so it is shown over "unsaved changes", in red.
+// The outcome of a save goes to the window's snackbar (Snack.qml), where it
+// is shown in full: notice is what a finished save says ("saved"), in
+// noticeColor, fading by itself; error is a save refused or failed, in red,
+// staying until dismissed. Only the draft's state stays here, beside the
+// buttons: "unsaved changes", or what is wrong when Save is held back.
Row {
id: bar
property bool dirty: false
@@ -37,18 +38,17 @@ Row {
signal discard
spacing: 8
+ onNoticeChanged: if (bar.notice !== "") Snack.show(bar.notice, bar.noticeColor, false)
+ onErrorChanged: if (bar.error !== "") Snack.error(bar.error)
+
Text {
anchors.verticalCenter: parent.verticalCenter
- visible: bar.error !== "" || bar.dirty || bar.notice !== ""
- // ponytail: a long message elides; the header has one line.
+ visible: bar.dirty
+ // ponytail: a long note elides; the header has one line.
width: Math.min(implicitWidth, 420)
elide: Text.ElideRight
- text: bar.error !== "" ? bar.error
- : !bar.dirty ? bar.notice
- : !bar.valid ? (bar.note || "fix the fields marked red") : "unsaved changes"
- color: bar.error !== "" ? Theme.red
- : !bar.dirty ? bar.noticeColor
- : bar.valid ? Theme.yellow : Theme.red
+ text: !bar.valid ? (bar.note || "fix the fields marked red") : "unsaved changes"
+ color: bar.valid ? Theme.yellow : Theme.red
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
}
Button {
diff --git a/desktop/Settings.qml b/desktop/Settings.qml
index 2ba8cb6..507960b 100644
--- a/desktop/Settings.qml
+++ b/desktop/Settings.qml
@@ -91,6 +91,8 @@ Scope {
else root.close();
}
function close() { root.open = false; root.page = ""; root.query = ""; }
+ // A message belongs to the page that said it.
+ onPageChanged: Snack.clear()
LazyLoader {
active: root.open
@@ -613,6 +615,13 @@ Scope {
when: pageLoader.item !== null
}
}
+
+ // Every page's save and upload results, in full. See Snack.qml.
+ Snackbar {
+ anchors { bottom: pageLoader.bottom; horizontalCenter: pageLoader.horizontalCenter }
+ width: Math.min(560, pageLoader.width - 32)
+ z: 10
+ }
}
}
}
diff --git a/desktop/Snack.qml b/desktop/Snack.qml
new file mode 100644
index 0000000..b955ad2
--- /dev/null
+++ b/desktop/Snack.qml
@@ -0,0 +1,41 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+pragma Singleton
+
+import Quickshell
+import QtQuick
+
+// The settings window's one message, shown by Snackbar.qml at the foot of the
+// window: what a save, an apply or an upload said. A page calls ok(), wait()
+// or error(); a new message replaces the last. ok() fades by itself, wait()
+// and error() stay until dismissed or replaced. SaveBar forwards its notice
+// and error here, so a page using it needs nothing more.
+Singleton {
+ id: root
+
+ property string text: ""
+ property color color: Theme.green
+ property bool sticky: false
+ // Bumped on every show, so the same text twice restarts the fade.
+ property int serial: 0
+
+ function show(text, color, sticky) {
+ root.text = text;
+ root.color = color;
+ root.sticky = !!sticky;
+ root.serial++;
+ }
+ function ok(text) { root.show(text, Theme.green, false); }
+ function wait(text) { root.show(text, Theme.yellow, true); }
+ function error(text) { root.show(text, Theme.red, true); }
+ function clear() { root.text = ""; }
+}
diff --git a/desktop/Snackbar.qml b/desktop/Snackbar.qml
new file mode 100644
index 0000000..467ac73
--- /dev/null
+++ b/desktop/Snackbar.qml
@@ -0,0 +1,71 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import QtQuick
+
+// Snack's message at the foot of the settings window: wrapped in full, a
+// stripe in its colour, a cross to dismiss. Place it once per window.
+Rectangle {
+ id: bar
+
+ readonly property bool shown: Snack.text !== ""
+
+ implicitHeight: Math.max(msg.implicitHeight, 20) + 20
+ radius: 10
+ color: Theme.surface
+ border.width: 1
+ border.color: Qt.alpha(Snack.color, 0.6)
+ opacity: shown ? 1 : 0
+ visible: opacity > 0
+ Behavior on opacity { NumberAnimation { duration: 160 } }
+
+ Timer {
+ id: fade
+ interval: 6000
+ onTriggered: Snack.clear()
+ }
+ Connections {
+ target: Snack
+ function onSerialChanged() { if (Snack.sticky) fade.stop(); else fade.restart(); }
+ }
+
+ Rectangle {
+ anchors { left: parent.left; top: parent.top; bottom: parent.bottom; margins: 6 }
+ width: 4
+ radius: 2
+ color: Snack.color
+ }
+
+ Text {
+ id: msg
+ anchors { left: parent.left; leftMargin: 20; right: close.left; rightMargin: 12; verticalCenter: parent.verticalCenter }
+ wrapMode: Text.Wrap
+ textFormat: Text.PlainText
+ text: Snack.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ color: Theme.text
+ }
+
+ Text {
+ id: close
+ anchors { right: parent.right; rightMargin: 12; verticalCenter: parent.verticalCenter }
+ text: "" // nf-fa-xmark
+ font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 2 }
+ color: hit.containsMouse ? Theme.text : Theme.subtext
+ MouseArea {
+ id: hit
+ anchors { fill: parent; margins: -6 }
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: Snack.clear()
+ }
+ }
+}
diff --git a/desktop/modules/sonix/BootlogoModule.qml b/desktop/modules/sonix/BootlogoModule.qml
index 8778ce3..ea39376 100644
--- a/desktop/modules/sonix/BootlogoModule.qml
+++ b/desktop/modules/sonix/BootlogoModule.qml
@@ -28,7 +28,7 @@ Module {
actions: Component {
Row {
spacing: 8
- // The status is in the page, under Output, where it wraps.
+ // Results go to the window's snackbar (Snack.qml).
Button {
text: tab.busy ? "Busy…" : "Save"
enabled: tab.canSave
diff --git a/desktop/modules/sonix/BootlogoTab.qml b/desktop/modules/sonix/BootlogoTab.qml
index 4a34dc7..96454a2 100644
--- a/desktop/modules/sonix/BootlogoTab.qml
+++ b/desktop/modules/sonix/BootlogoTab.qml
@@ -37,10 +37,10 @@ Row {
property bool busy: false
property bool waiting: false
- property string status: ""
- property bool statusError: false
readonly property bool canSave: canvas.ready && !tab.busy
- function say(t, err) { tab.status = t; tab.statusError = !!err; }
+ // To the window's snackbar (Snack.qml): yellow while waiting for Wi-Fi
+ // transfer, red on failure, green otherwise.
+ function say(t, err) { if (tab.waiting) Snack.wait(t); else if (err) Snack.error(t); else Snack.ok(t); }
readonly property var swatches: ["#ffffff", "#000000", "#3584e4", "#ff7800", "#e5a50a",
"#2ec27e", "#9141ac", "#d56199", "udt"]
@@ -319,15 +319,6 @@ Row {
enabled: Sonix.present && !tab.busy && !tab.waiting
onClicked: pickSaved.open()
}
- // Save and upload results, in full: the header has no room for them.
- Text {
- visible: tab.status !== ""
- width: parent.width
- wrapMode: Text.Wrap
- text: tab.status
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
- color: tab.waiting ? Theme.yellow : tab.statusError ? Theme.red : Theme.green
- }
Text {
width: parent.width
wrapMode: Text.Wrap