From 8c7a41a8edc97c22fe6d197c7a8b2232a12a4fec Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 02:26:54 +0000 Subject: [PATCH] Skip restoring a toggle already in its remembered state [patch] LoadPreferences called every persisted toggle's setter with the remembered value. A setter written as a flip (as the class example's is) then inverted a toggle whose remembered value matched its current state, and a setter that takes a resource on true took it a second time. Restore now reads the toggle's getter first and leaves it alone when it already matches; the getter read sits inside the same refusal handling as the setter. Fixes ktsu-dev/TrayApp#61 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW4brDwN4YAzA6oCKV6 --- TrayApp.Test/TrayAppBuilderTests.cs | 61 +++++++++++++++++++++++++++++ TrayApp/Menu/TrayToggleItem.cs | 6 +++ TrayApp/TrayAppBuilder.cs | 8 +++- 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/TrayApp.Test/TrayAppBuilderTests.cs b/TrayApp.Test/TrayAppBuilderTests.cs index 44607aa..2f72183 100644 --- a/TrayApp.Test/TrayAppBuilderTests.cs +++ b/TrayApp.Test/TrayAppBuilderTests.cs @@ -200,6 +200,67 @@ public async Task LoadPreferences_RestoresOnlyThePersistedToggles() Assert.IsFalse(forgotten); } + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public async Task LoadPreferences_WhenTheToggleIsAlreadyInTheRememberedState_DoesNotCallTheSetter(bool state) + { + InMemoryPersistenceProvider provider = new(); + + using (DebouncedPreferenceStore seed = new(provider, "tray", Debounce)) + { + await seed.LoadAsync().ConfigureAwait(false); + seed.Set("awake", state); + await seed.FlushAsync().ConfigureAwait(false); + } + + bool awake = state; + int setterCalls = 0; + + // The setter ignores its argument and flips, as the class example's does. + TrayAppDefinition app = TrayAppBuilder.Create("demo") + .Toggle("Keep awake", () => awake, _ => + { + setterCalls++; + awake = !awake; + }, persistAs: "awake") + .Preferences(provider) + .Build(); + + using DebouncedPreferenceStore store = new(provider, "tray", Debounce); + + Assert.IsNull(TrayAppBuilder.LoadPreferences(app, store)); + Assert.AreEqual(state, awake); + Assert.AreEqual(0, setterCalls); + } + + [TestMethod] + public async Task LoadPreferences_WhenTheGetterThrows_KeepsGoingAndHandsBackTheMessage() + { + InMemoryPersistenceProvider provider = new(); + + using (DebouncedPreferenceStore seed = new(provider, "tray", Debounce)) + { + await seed.LoadAsync().ConfigureAwait(false); + seed.Set("awake", true); + seed.Set("display", true); + await seed.FlushAsync().ConfigureAwait(false); + } + + bool display = false; + + TrayAppDefinition app = TrayAppBuilder.Create("demo") + .Toggle("Keep awake", () => throw new InvalidOperationException("no inhibitor here"), _ => { }, persistAs: "awake") + .Toggle("Keep display awake", () => display, value => display = value, persistAs: "display") + .Preferences(provider) + .Build(); + + using DebouncedPreferenceStore store = new(provider, "tray", Debounce); + + Assert.AreEqual("no inhibitor here", TrayAppBuilder.LoadPreferences(app, store)); + Assert.IsTrue(display); + } + [TestMethod] public async Task LoadPreferences_ThenOptions_LetsAnExplicitSwitchWin() { diff --git a/TrayApp/Menu/TrayToggleItem.cs b/TrayApp/Menu/TrayToggleItem.cs index d3650d2..4232ecf 100644 --- a/TrayApp/Menu/TrayToggleItem.cs +++ b/TrayApp/Menu/TrayToggleItem.cs @@ -63,6 +63,12 @@ public override void Refresh() /// public void Set(bool value) => setState(value); + /// + /// Reads what the tool currently reports, without touching . + /// + /// The getter's value. The getter belongs to the tool and may throw. + internal bool ReadState() => getState(); + /// /// Sets the toggle to the opposite of what the tool currently reports. /// diff --git a/TrayApp/TrayAppBuilder.cs b/TrayApp/TrayAppBuilder.cs index 73cf5ca..df99fd8 100644 --- a/TrayApp/TrayAppBuilder.cs +++ b/TrayApp/TrayAppBuilder.cs @@ -535,7 +535,13 @@ private static async Task FinishAsync(int exitCode, DebouncedPreferenceStor try { - toggle.Set(remembered); + // A restore asks for a state, where a click asks for a change, so a toggle already in the + // remembered state is left alone: a setter written as a flip (as the class example's is) would + // otherwise invert it, and one that takes a resource would take it a second time. + if (toggle.ReadState() != remembered) + { + toggle.Set(remembered); + } } catch (Exception ex) when (FatalError.IsNotFatal(ex)) {