From f73b88072b7abad720923dd32bae6a1e98bf64d5 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 17 Oct 2026 11:37:12 +0200 Subject: settings: every page commits from the header The Mail, Assistant and Ask presets pages got a Save and Discard pair in the page header; the rest still had their own buttons wherever their page put them: User's Save at the bottom, Wallpaper's Apply and Reset beside the mock screens, Icons' Apply and Reset under the cursor cards, HyprSunset's and HyprIdle's Save + Restart in the page. They now all sit in the header SaveBar, which takes the page's own verbs (Apply and Reset, Save + Restart) and a busy state. HyprSunset and HyprIdle had no notion of an unsaved change, so their button was always live. They now count as changed when what save() would write differs from what was read, which enables Save only then, gives Discard (a reload) something to drop, and marks them in the sidebar; a window open no longer reloads them over unsaved edits. The staged cursor moved from IconsTab into the Icons singleton so the header and the sidebar can see it. Git and autostart act a row at a time, and a theme applies on click, so those have no bar. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/appearance/Hypridle.qml | 8 ++++++- desktop/modules/appearance/Hyprsunset.qml | 12 +++++++++- desktop/modules/appearance/Icons.qml | 3 +++ desktop/modules/appearance/IconsModule.qml | 7 ++++++ desktop/modules/appearance/IconsTab.qml | 32 ++++++-------------------- desktop/modules/appearance/IdleModule.qml | 8 ++++++- desktop/modules/appearance/IdleTab.qml | 6 +---- desktop/modules/appearance/README.md | 3 ++- desktop/modules/appearance/SunsetModule.qml | 8 ++++++- desktop/modules/appearance/SunsetTab.qml | 6 +---- desktop/modules/appearance/WallpaperModule.qml | 5 ++++ desktop/modules/appearance/WallpaperTab.qml | 15 +++--------- 12 files changed, 61 insertions(+), 52 deletions(-) (limited to 'desktop/modules/appearance') diff --git a/desktop/modules/appearance/Hypridle.qml b/desktop/modules/appearance/Hypridle.qml index a1d3ecd..5f03c35 100644 --- a/desktop/modules/appearance/Hypridle.qml +++ b/desktop/modules/appearance/Hypridle.qml @@ -30,6 +30,10 @@ Singleton { property string tail: "" property string notice: "" property bool loaded: false + // The file as last read or written: dirty is what save() would write + // differing from it. Discard is refresh(). + property string saved: "" + readonly property bool dirty: root.loaded && root.serialize(root.prefix, root.listeners, root.tail) !== root.saved function getField(body, key) { const m = body.match(new RegExp(`^\\s*${key}\\s*=\\s*(.+?)\\s*$`, "m")); @@ -146,6 +150,7 @@ listener { root.prefix = p.prefix; root.listeners = p.listeners; root.tail = p.tail; + root.saved = root.serialize(p.prefix, p.listeners, p.tail); root.loaded = true; } } @@ -155,7 +160,8 @@ listener { function save() { if (!root.loaded) return; // not loaded yet: writing would truncate the file - confFile.setText(root.serialize(root.prefix, root.listeners, root.tail)); + root.saved = root.serialize(root.prefix, root.listeners, root.tail); + confFile.setText(root.saved); Quickshell.execDetached(["sh", "-c", "pkill -x hypridle; setsid -f hypridle"]); root.notice = "saved; idle timers reset"; } diff --git a/desktop/modules/appearance/Hyprsunset.qml b/desktop/modules/appearance/Hyprsunset.qml index 5886735..4befcb5 100644 --- a/desktop/modules/appearance/Hyprsunset.qml +++ b/desktop/modules/appearance/Hyprsunset.qml @@ -39,6 +39,12 @@ Singleton { property bool busy: false property bool confLoaded: false property bool appLoaded: false + // The two files as last read or written, to tell an unsaved edit: dirty + // is what save() would write differing from them. Discard is refresh(). + property string savedConf: "" + property string savedApp: "" + readonly property bool dirty: root.confLoaded && root.appLoaded + && (root.serializeProfiles(root.profiles) !== root.savedConf || root.appConfText() !== root.savedApp) readonly property string header: "# Managed by hyprsunset-qt. Edits here are overwritten on save.\n" @@ -166,6 +172,7 @@ profile { stdout: StdioCollector { onStreamFinished: { root.profiles = root.parseProfiles(text); + root.savedConf = root.serializeProfiles(root.profiles); root.confLoaded = true; } } @@ -202,6 +209,7 @@ profile { root.cachePathRaw = c["cache.path"] ?? "~/.config/hyprsunset-qt/sun.json"; root.cachePath = root.expandTilde(root.cachePathRaw); root.daemonCommand = c["daemon.command"] ?? "hyprsunset"; + root.savedApp = root.appConfText(); root.appLoaded = true; } } @@ -242,7 +250,9 @@ profile { root.notice = `invalid gamma: ${p.gamma}`; return; } } - confFile.setText(root.serializeProfiles(root.profiles)); + root.savedConf = root.serializeProfiles(root.profiles); + root.savedApp = root.appConfText(); + confFile.setText(root.savedConf); root.saveSettings(); root.restart(); root.notice = "saved + restarted"; diff --git a/desktop/modules/appearance/Icons.qml b/desktop/modules/appearance/Icons.qml index a6eaec5..fa2753b 100644 --- a/desktop/modules/appearance/Icons.qml +++ b/desktop/modules/appearance/Icons.qml @@ -110,6 +110,9 @@ for name in sys.argv[2:]: property var cursorPreview: ({}) property bool scanning: true property string notice: "" + // A cursor theme clicked but not applied, "" for none. Kept here rather + // than in the page, so the header's Apply and the sidebar dot can see it. + property string pendingCursor: "" // Pure: classify works on entries shaped { name, index, cursors, manifest } // so it can be tested without touching the filesystem. diff --git a/desktop/modules/appearance/IconsModule.qml b/desktop/modules/appearance/IconsModule.qml index bf3722b..94132db 100644 --- a/desktop/modules/appearance/IconsModule.qml +++ b/desktop/modules/appearance/IconsModule.qml @@ -19,6 +19,7 @@ Module { // nf-fa-mouse_pointer, present in Inconsolata Nerd Font. icon: "\uf245" settings: true + dirty: Icons.pendingCursor !== "" group: "Appearance" keywords: ["cursor", "pointer", "size", "gtk", "qt", "theme"] searchItems: Icons.iconThemes.map(n => ({ text: "icons: " + n, key: "icon:" + n })) @@ -29,6 +30,12 @@ Module { page: Component { Page { title: "Icons & cursor" + // The staged cursor theme; an icon theme applies on click. + actions: Component { + SaveBar { saveText: "Apply"; discardText: "Reset"; dirty: Icons.pendingCursor !== "" + onSave: { Icons.applyCursor(Icons.pendingCursor); Icons.pendingCursor = ""; } + onDiscard: Icons.pendingCursor = "" } + } IconsTab { width: parent.width } } } diff --git a/desktop/modules/appearance/IconsTab.qml b/desktop/modules/appearance/IconsTab.qml index 777ea51..1df9cb0 100644 --- a/desktop/modules/appearance/IconsTab.qml +++ b/desktop/modules/appearance/IconsTab.qml @@ -22,7 +22,6 @@ Item { // persists the theme to org.gnome.desktop.interface cursor-theme, so // hovering wrote the setting and a close left the last hovered theme set. // The four rendered shapes on each card are the preview. - property string pendingCursor: "" Column { id: col @@ -139,7 +138,7 @@ Item { required property string modelData objectName: "cursor:" + modelData readonly property bool current: Icons.currentCursor === modelData - readonly property bool staged: pendingCursor === modelData + readonly property bool staged: Icons.pendingCursor === modelData width: 230; height: 150; radius: 10 color: current || staged ? Qt.alpha(Theme.accent, 0.2) : Qt.alpha(Theme.surface, 0.35) border.width: current || staged ? 2 : 1 @@ -150,7 +149,7 @@ Item { MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: pendingCursor = modelData + onClicked: Icons.pendingCursor = modelData } Column { @@ -191,28 +190,11 @@ Item { } } - Row { - spacing: 8 - Text { - anchors.verticalCenter: parent.verticalCenter - text: pendingCursor !== "" ? "click Apply to set" : "click a cursor theme to stage it" - color: Theme.overlay - font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } - } - Tab { - text: "Apply" - selected: pendingCursor !== "" - enabled: pendingCursor !== "" - onClicked: { - Icons.applyCursor(pendingCursor); - pendingCursor = ""; - } - } - Tab { - text: "Reset" - enabled: pendingCursor !== "" - onClicked: pendingCursor = "" - } + // Apply and Reset are in the page header, with every settings page's. + Text { + text: Icons.pendingCursor !== "" ? "Apply, in the header, sets it" : "click a cursor theme to stage it" + color: Theme.overlay + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Text { diff --git a/desktop/modules/appearance/IdleModule.qml b/desktop/modules/appearance/IdleModule.qml index 4b27cd3..2e4f32a 100644 --- a/desktop/modules/appearance/IdleModule.qml +++ b/desktop/modules/appearance/IdleModule.qml @@ -19,14 +19,20 @@ Module { // nf-fa-hourglass, present in Inconsolata Nerd Font. icon: "\uf254" settings: true + dirty: Hypridle.dirty group: "Desktop" keywords: ["hypridle", "lock", "timeout", "screen", "dpms", "suspend", "sleep"] - function refresh() { Hypridle.refresh(); } + // Not over unsaved edits: the reload would drop them. + function refresh() { if (!Hypridle.dirty) Hypridle.refresh(); } page: Component { Page { title: "HyprIdle" + actions: Component { + SaveBar { saveText: "Save + Restart"; dirty: Hypridle.dirty + onSave: Hypridle.save(); onDiscard: Hypridle.refresh() } + } IdleTab { width: parent.width } } } diff --git a/desktop/modules/appearance/IdleTab.qml b/desktop/modules/appearance/IdleTab.qml index 1199543..fdb39ec 100644 --- a/desktop/modules/appearance/IdleTab.qml +++ b/desktop/modules/appearance/IdleTab.qml @@ -82,11 +82,7 @@ Item { } } - Tab { - text: "Save + Restart hypridle" - selected: true - onClicked: Hypridle.save() - } + // Save + Restart is in the page header, with every settings page's. Text { width: parent.width diff --git a/desktop/modules/appearance/README.md b/desktop/modules/appearance/README.md index 51e5f09..b80b564 100644 --- a/desktop/modules/appearance/README.md +++ b/desktop/modules/appearance/README.md @@ -65,7 +65,8 @@ waybar-theme-udt's `install.sh` or signals waybar. ## Wallpapers Clicking a thumbnail **stages** it rather than setting it, so both screens can -be composed before anything changes, and Apply then makes a single +be composed before anything changes, and Apply, in the page header like +every settings page's commit, then makes a single `wallp --set H=… V=…` call. The mock screens above the list show what the desk would look like: the staged pick where there is one, what is currently set where there is not, and the hovered thumbnail on the targeted screen while the diff --git a/desktop/modules/appearance/SunsetModule.qml b/desktop/modules/appearance/SunsetModule.qml index 557a2a4..d55d874 100644 --- a/desktop/modules/appearance/SunsetModule.qml +++ b/desktop/modules/appearance/SunsetModule.qml @@ -19,14 +19,20 @@ Module { // nf-fa-moon_o, present in Inconsolata Nerd Font. icon: "\uf186" settings: true + dirty: Hyprsunset.dirty group: "Desktop" keywords: ["hyprsunset", "night", "light", "temperature", "gamma", "location", "profile"] - function refresh() { Hyprsunset.refresh(); } + // Not over unsaved edits: the reload would drop them. + function refresh() { if (!Hyprsunset.dirty) Hyprsunset.refresh(); } page: Component { Page { title: "HyprSunset" + actions: Component { + SaveBar { saveText: "Save + Restart"; dirty: Hyprsunset.dirty + onSave: Hyprsunset.save(); onDiscard: Hyprsunset.refresh() } + } SunsetTab { width: parent.width } } } diff --git a/desktop/modules/appearance/SunsetTab.qml b/desktop/modules/appearance/SunsetTab.qml index 657a920..26ec264 100644 --- a/desktop/modules/appearance/SunsetTab.qml +++ b/desktop/modules/appearance/SunsetTab.qml @@ -150,11 +150,7 @@ Item { temperature: null, gamma: null }]) } Tab { text: "Live preview"; onClicked: Hyprsunset.preview() } - Tab { - text: "Save + Restart" - selected: true - onClicked: Hyprsunset.save() - } + // Save + Restart is in the page header, with every settings page's. } Text { diff --git a/desktop/modules/appearance/WallpaperModule.qml b/desktop/modules/appearance/WallpaperModule.qml index c21d94d..cbc7158 100644 --- a/desktop/modules/appearance/WallpaperModule.qml +++ b/desktop/modules/appearance/WallpaperModule.qml @@ -20,6 +20,7 @@ Module { // nf-fa-picture_o, present in Inconsolata Nerd Font. icon: "\uf03e" settings: true + dirty: Wallpapers.dirty group: "Appearance" keywords: ["background", "screen", "monitor", "wallp", "wallhaven", "horizontal", "vertical"] @@ -29,6 +30,10 @@ Module { Page { id: pg title: "Wallpaper" + actions: Component { + SaveBar { saveText: "Apply"; discardText: "Reset"; dirty: Wallpapers.dirty; busy: Wallpapers.applying + onSave: Wallpapers.apply(); onDiscard: Wallpapers.clearPending() } + } WallpaperTab { width: parent.width; height: pg.viewHeight; onDismiss: pg.dismiss() } } } diff --git a/desktop/modules/appearance/WallpaperTab.qml b/desktop/modules/appearance/WallpaperTab.qml index 9788ba2..af3c23b 100644 --- a/desktop/modules/appearance/WallpaperTab.qml +++ b/desktop/modules/appearance/WallpaperTab.qml @@ -85,7 +85,7 @@ Column { width: parent.width wrapMode: Text.Wrap text: tab.hovering !== "" ? Wallpapers.label(tab.hovering) - : Wallpapers.dirty ? "click Apply to set" + : Wallpapers.dirty ? "Apply, in the header, sets it" : "click a wallpaper to stage it" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } color: Theme.overlay @@ -111,17 +111,8 @@ Column { width: parent.width spacing: 8 - Tab { - text: Wallpapers.applying ? "applying…" : "Apply" - selected: Wallpapers.dirty && !Wallpapers.applying - enabled: Wallpapers.dirty && !Wallpapers.applying - onClicked: Wallpapers.apply() - } - Tab { - text: "Reset" - enabled: Wallpapers.dirty && !Wallpapers.applying - onClicked: Wallpapers.clearPending() - } + // Apply and Reset are in the page header, with every settings + // page's. // wallhaven-dl searches wallhaven.cc and finds larger // copies of low-res wallpapers. Launched through its // desktop entry, which needs no ~/bin on quickshell's PATH -- cgit v1.2.3