From 670452de94e0f3e0bf5719acec28a9b02e929642 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 29 Oct 2026 17:13:53 +0100 Subject: wallpaper: engine page commits like the others, Start changes at once The page was the only settings page without the header's buttons, and it switched the engine from a checkbox beside the interval. Both looked like the engine had been started when it had not: - The minutes field saved on editingFinished, which needs Enter or a focus change. Clicking the checkbox takes neither, so a typed 1 was left at 30 and the first change was half an hour away. - Even at the right interval, the first change waited one full interval, so starting the engine did nothing visible. The interval and the two sources are now a draft in WallEngine, updated on every keystroke, and the header carries the state ("running, next change at hh:mm", "stopped", "unsaved changes") with Reset, Stop and Start, which reads Apply while running. Start commits the draft, sets the homes and changes the wallpapers straight away; Apply commits and restarts the timer. Hearts stay undrafted, written as clicked. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/appearance/WallEngine.qml | 57 ++++++++++++++++++++++--- desktop/modules/appearance/WallEngineModule.qml | 33 ++++++++++++++ desktop/modules/appearance/WallEngineTab.qml | 28 ++++++------ 3 files changed, 100 insertions(+), 18 deletions(-) (limited to 'desktop') diff --git a/desktop/modules/appearance/WallEngine.qml b/desktop/modules/appearance/WallEngine.qml index fe45f7b..54705f6 100644 --- a/desktop/modules/appearance/WallEngine.qml +++ b/desktop/modules/appearance/WallEngine.qml @@ -33,7 +33,6 @@ Singleton { function listed(path, which) { return list(which).indexOf(path) >= 0; } // A gallery folder by its Wallpapers.groups key, "" for the hearted list. function folder(which) { return which === "V" ? cfg.folderV : cfg.folderH; } - function setFolder(which, key) { Prefs.setWallEngine(which === "V" ? { folderV: key } : { folderH: key }); } // What a tick draws from. A folder that is no longer in the gallery // gives nothing, so that screen is left alone rather than falling back // to the list behind the user's back. @@ -65,9 +64,46 @@ Singleton { } } - function setMinutes(m) { - if (Number.isInteger(m) && m >= 1 && m !== cfg.minutes) Prefs.setWallEngine({ minutes: m }); + // The page's draft, like every settings page: the interval and the two + // sources change here and reach Prefs on Start or Apply. Reset binds + // them back to what is saved. Hearts are not drafted: the picker writes + // them as they are clicked. + property int draftMinutes: cfg.minutes + property string draftFolderH: cfg.folderH + property string draftFolderV: cfg.folderV + readonly property bool dirty: draftMinutes !== cfg.minutes + || draftFolderH !== cfg.folderH || draftFolderV !== cfg.folderV + function draftFolder(which) { return which === "V" ? draftFolderV : draftFolderH; } + function setDraftFolder(which, key) { + if (which === "V") draftFolderV = key; + else draftFolderH = key; } + function setDraftMinutes(m) { + if (Number.isInteger(m) && m >= 1) draftMinutes = m; + } + function reset() { + draftMinutes = Qt.binding(() => cfg.minutes); + draftFolderH = Qt.binding(() => cfg.folderH); + draftFolderV = Qt.binding(() => cfg.folderV); + } + function commit() { + Prefs.setWallEngine({ minutes: draftMinutes, folderH: draftFolderH, folderV: draftFolderV }); + reset(); + } + + // Start changes the wallpapers at once: the first timer change would + // otherwise be a whole interval away, which read as the engine not + // working. + function start() { + commit(); + setOn(true); + next(); + } + function apply() { + commit(); + restartTimer(); + } + function stop() { setOn(false); } // ponytail: in memory, so a restart starts a new round. property var bagH: [] @@ -88,7 +124,15 @@ Singleton { function next() { tick(); + restartTimer(); + } + + // When the timer next fires, for the page's header; 0 while off. Timer + // has no such property, so it is kept beside it. + property double nextAt: 0 + function restartTimer() { timer.restart(); + nextAt = Date.now() + timer.interval; } // A switch-off during a run cannot start its own; it goes when that run @@ -106,7 +150,10 @@ Singleton { interval: root.cfg.minutes * 60000 repeat: true running: root.on - onTriggered: root.tick() + onTriggered: { root.nextAt = Date.now() + interval; root.tick(); } + // A running Timer restarts on a new interval. + onIntervalChanged: if (running) root.nextAt = Date.now() + interval + onRunningChanged: root.nextAt = running ? Date.now() + interval : 0 } Connections { @@ -116,7 +163,7 @@ Singleton { if (!root.on) return; Prefs.setWallEngine({ homeH: h !== "" ? h : root.cfg.homeH, homeV: v !== "" ? v : root.cfg.homeV }); - timer.restart(); + root.restartTimer(); } function onFinished() { if (!root.homeWaiting || root.on) return; diff --git a/desktop/modules/appearance/WallEngineModule.qml b/desktop/modules/appearance/WallEngineModule.qml index e907695..5b8a39c 100644 --- a/desktop/modules/appearance/WallEngineModule.qml +++ b/desktop/modules/appearance/WallEngineModule.qml @@ -19,6 +19,7 @@ Module { // nf-fa-shuffle, present in Inconsolata Nerd Font. icon: "\uf074" settings: true + dirty: WallEngine.dirty group: "Appearance" keywords: ["wallpaper", "timer", "rotate", "rotation", "shuffle", "slideshow", "heart"] @@ -30,6 +31,38 @@ Module { page: Component { Page { title: "Wallpaper engine" + // SaveBar's place, not SaveBar: the engine is started and stopped + // here as well as saved, and its state is what the header reports. + actions: Component { + Row { + spacing: 8 + Text { + anchors.verticalCenter: parent.verticalCenter + text: WallEngine.dirty ? "unsaved changes" + : WallEngine.on ? "running, next change at " + + Qt.formatTime(new Date(WallEngine.nextAt), "hh:mm") + : "stopped" + color: WallEngine.dirty ? Theme.yellow : WallEngine.on ? Theme.green : Theme.overlay + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + Button { + visible: WallEngine.dirty + text: "Reset" + onClicked: WallEngine.reset() + } + Button { + visible: WallEngine.on + text: "Stop" + enabled: !Wallpapers.applying + onClicked: WallEngine.stop() + } + Button { + text: WallEngine.on ? "Apply" : "Start" + enabled: (!WallEngine.on || WallEngine.dirty) && !Wallpapers.applying + onClicked: WallEngine.on ? WallEngine.apply() : WallEngine.start() + } + } + } WallEngineTab { width: parent.width } } } diff --git a/desktop/modules/appearance/WallEngineTab.qml b/desktop/modules/appearance/WallEngineTab.qml index caa01f4..3f72d83 100644 --- a/desktop/modules/appearance/WallEngineTab.qml +++ b/desktop/modules/appearance/WallEngineTab.qml @@ -22,20 +22,21 @@ Item { Row { spacing: 8 - Toggle { + Text { anchors.verticalCenter: parent.verticalCenter - checked: WallEngine.on - label: "Change the wallpapers every" - onToggled: WallEngine.setOn(checked) + text: "Change the wallpapers every" + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Field { anchors.verticalCenter: parent.verticalCenter width: 50 - value: String(WallEngine.cfg.minutes) - onEdited: { - WallEngine.setMinutes(Number(value)); - value = String(WallEngine.cfg.minutes); - } + value: String(WallEngine.draftMinutes) + // On every keystroke, not on edited: that waits for Enter + // or a focus change, and clicking Start takes neither, so a + // typed interval was silently left at the old one. + onValueChanged: WallEngine.setDraftMinutes(Number(value)) + onEdited: value = String(WallEngine.draftMinutes) } Text { anchors.verticalCenter: parent.verticalCenter @@ -54,7 +55,7 @@ Item { width: col.width wrapMode: Text.Wrap text: "Timer changes recolour the desktop and leave GRUB, the homepage and the login screen alone. " - + "Switching off puts back each screen's home wallpaper, the one last applied by hand, with a full run." + + "Stop puts back each screen's home wallpaper, the one last applied by hand, with a full run." color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } @@ -67,7 +68,8 @@ Item { required property var modelData readonly property var paths: WallEngine.list(modelData.which) readonly property string home: modelData.which === "V" ? WallEngine.cfg.homeV : WallEngine.cfg.homeH - readonly property string folder: WallEngine.folder(modelData.which) + // The draft's source: what Start or Apply will use. + readonly property string folder: WallEngine.draftFolder(modelData.which) readonly property var group: Wallpapers.groups.find(g => g.key === folder) ?? null width: col.width spacing: 8 @@ -90,7 +92,7 @@ Item { Tab { text: "Hearted" selected: screen.folder === "" - onClicked: WallEngine.setFolder(screen.modelData.which, "") + onClicked: WallEngine.setDraftFolder(screen.modelData.which, "") } Repeater { model: Wallpapers.groups @@ -98,7 +100,7 @@ Item { required property var modelData text: modelData.name selected: screen.folder === modelData.key - onClicked: WallEngine.setFolder(screen.modelData.which, modelData.key) + onClicked: WallEngine.setDraftFolder(screen.modelData.which, modelData.key) } } } -- cgit v1.2.3