From 4a88033695640f80e020b4f84e08dd1cf740a9fc Mon Sep 17 00:00:00 2001 From: doyaGu Date: Sat, 3 Oct 2026 09:12:36 -0400 Subject: [PATCH] fix(settings): apply a theme as soon as it is selected and restore it on cancel PLAN 5.6 asks for the theme to take effect immediately, but it was applied only on OK. Selecting a theme now previews it; OK saves it, and cancel, Escape or closing the window restores the theme the dialog opened with. --- .../SettingsDialogViewModelTests.cs | 63 +++++++++++++++++++ .../Dialogs/SettingsDialogViewModel.cs | 31 +++++++-- YKanBan/Views/Dialogs/SettingsDialog.axaml.cs | 17 +++++ docs/ACCEPTANCE.md | 3 +- docs/PLAN.md | 2 +- 5 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 YKanBan.Tests/ViewModels/SettingsDialogViewModelTests.cs diff --git a/YKanBan.Tests/ViewModels/SettingsDialogViewModelTests.cs b/YKanBan.Tests/ViewModels/SettingsDialogViewModelTests.cs new file mode 100644 index 0000000..5304a48 --- /dev/null +++ b/YKanBan.Tests/ViewModels/SettingsDialogViewModelTests.cs @@ -0,0 +1,63 @@ +using YKanBan.Storage.AppData; +using YKanBan.ViewModels.Dialogs; + +namespace YKanBan.Tests.ViewModels; + +/// +/// Tests for the settings dialog: the theme is previewed as soon as it is +/// selected, saved on accept and restored on cancel. +/// +[TestClass] +public class SettingsDialogViewModelTests +{ + [TestMethod] + public void OpeningTheDialogAppliesNothing() + { + var applied = new List(); + + _ = new SettingsDialogViewModel(new AppConfig(), AppConfig.DefaultLanguage, applied.Add); + + Assert.AreEqual(0, applied.Count); + } + + [TestMethod] + public void SelectingAThemeAppliesItImmediatelyWithoutSavingIt() + { + var config = new AppConfig(); + var applied = new List(); + var dialog = new SettingsDialogViewModel(config, AppConfig.DefaultLanguage, applied.Add); + + dialog.SelectedTheme = dialog.Themes.Single(option => option.Value.Equals(ThemeOption.Dark)); + + CollectionAssert.AreEqual(new[] { ThemeOption.Dark }, applied); + Assert.AreEqual(ThemeOption.FollowSystem, config.Theme); + } + + [TestMethod] + public void AcceptSavesThePreviewedTheme() + { + var config = new AppConfig(); + var applied = new List(); + var dialog = new SettingsDialogViewModel(config, AppConfig.DefaultLanguage, applied.Add); + dialog.SelectedTheme = dialog.Themes.Single(option => option.Value.Equals(ThemeOption.Light)); + + dialog.Accept(); + + Assert.AreEqual(ThemeOption.Light, config.Theme); + CollectionAssert.AreEqual(new[] { ThemeOption.Light }, applied); + } + + [TestMethod] + public void CancelRestoresTheThemeTheDialogOpenedWith() + { + var config = new AppConfig { Theme = ThemeOption.Light }; + var applied = new List(); + var dialog = new SettingsDialogViewModel(config, AppConfig.DefaultLanguage, applied.Add); + dialog.SelectedTheme = dialog.Themes.Single(option => option.Value.Equals(ThemeOption.Dark)); + + dialog.Cancel(); + + Assert.AreEqual(ThemeOption.Light, config.Theme); + CollectionAssert.AreEqual(new[] { ThemeOption.Dark, ThemeOption.Light }, applied); + } +} diff --git a/YKanBan/ViewModels/Dialogs/SettingsDialogViewModel.cs b/YKanBan/ViewModels/Dialogs/SettingsDialogViewModel.cs index 94f33fd..2d16065 100644 --- a/YKanBan/ViewModels/Dialogs/SettingsDialogViewModel.cs +++ b/YKanBan/ViewModels/Dialogs/SettingsDialogViewModel.cs @@ -5,23 +5,27 @@ namespace YKanBan.ViewModels.Dialogs; /// /// The settings dialog: language (restart to apply), theme and the four -/// confirmation-dialog toggles. Nothing is applied until the user accepts; -/// cancelling or closing the window discards the changes. +/// confirmation-dialog toggles. A theme selection is previewed immediately; +/// everything else waits until the user accepts. Cancelling or closing the +/// window discards the changes and restores the theme the dialog opened with. /// public sealed partial class SettingsDialogViewModel : ViewModelBase { private readonly AppConfig _config; private readonly string _startupLanguage; + private readonly Action _applyTheme; /// /// Initializes the dialog from the live configuration. /// /// The live application configuration. /// The language applied at startup, for the restart notice. - public SettingsDialogViewModel(AppConfig config, string startupLanguage) + /// Applies a theme to the running app; defaults to . + public SettingsDialogViewModel(AppConfig config, string startupLanguage, Action? applyTheme = null) { _config = config; _startupLanguage = startupLanguage; + _applyTheme = applyTheme ?? App.ApplyTheme; Languages = [ @@ -63,7 +67,8 @@ public sealed partial class SettingsDialogViewModel : ViewModelBase private OptionItem? selectedLanguage; /// - /// Gets or sets the selected theme; applied when the dialog is accepted. + /// Gets or sets the selected theme; applied to the running app as soon as it + /// changes, and saved to the configuration when the dialog is accepted. /// [ObservableProperty] private OptionItem? selectedTheme; @@ -110,7 +115,6 @@ public sealed partial class SettingsDialogViewModel : ViewModelBase if (SelectedTheme is not null) { _config.Theme = (ThemeOption)SelectedTheme.Value; - App.ApplyTheme(_config.Theme); } _config.Confirmations.DeleteCard = ConfirmDeleteCard; @@ -119,6 +123,23 @@ public sealed partial class SettingsDialogViewModel : ViewModelBase _config.Confirmations.DiscardEdit = ConfirmDiscardEdit; } + /// + /// Discards the working values and restores the theme the dialog opened with. + /// + public void Cancel() => _applyTheme(_config.Theme); + + /// + /// Previews a theme selection on the running app. + /// + /// The new selection. + partial void OnSelectedThemeChanged(OptionItem? value) + { + if (value is not null) + { + _applyTheme((ThemeOption)value.Value); + } + } + /// /// Refreshes the restart notice after a language selection change. /// diff --git a/YKanBan/Views/Dialogs/SettingsDialog.axaml.cs b/YKanBan/Views/Dialogs/SettingsDialog.axaml.cs index ba0cac9..ad8b21e 100644 --- a/YKanBan/Views/Dialogs/SettingsDialog.axaml.cs +++ b/YKanBan/Views/Dialogs/SettingsDialog.axaml.cs @@ -6,6 +6,8 @@ namespace YKanBan.Views.Dialogs; public partial class SettingsDialog : Window { + private bool _accepted; + public SettingsDialog() { InitializeComponent(); @@ -15,9 +17,24 @@ public partial class SettingsDialog : Window private void OnOkClick(object? sender, RoutedEventArgs e) { (DataContext as SettingsDialogViewModel)?.Accept(); + _accepted = true; Close(true); } /// Closes without applying anything. private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false); + + /// + /// Restores the previewed theme on every close that did not accept, + /// including the title-bar close button and Escape. + /// + /// The event arguments. + protected override void OnClosed(EventArgs e) + { + if (!_accepted) + { + (DataContext as SettingsDialogViewModel)?.Cancel(); + } + base.OnClosed(e); + } } diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md index 3e53acf..43220d3 100644 --- a/docs/ACCEPTANCE.md +++ b/docs/ACCEPTANCE.md @@ -30,7 +30,8 @@ ### A3 主题三态切换 -- [ ] 设置 → 主题 亮色 / 暗色 / 跟随系统 依次切换:**即时生效**,无重启提示 +- [ ] 设置 → 主题 亮色 / 暗色 / 跟随系统 依次切换:下拉框一选中就**即时生效**(无需点确认),无重启提示 +- [ ] 切换主题后点取消、按 Esc 或点标题栏关闭:恢复为打开设置时的主题;重启后仍是原主题 - [ ] 每种主题下检查:主窗口、看板列与卡片、标签 tab、全部模态、全部错误页颜色完整切换,无亮底暗字/暗底亮字残留 - [ ] 标签徽章在两种主题下均显示原色(§5.9 例外),文字颜色保持可读 - [ ] 跟随系统:程序运行中切换 Windows 深浅色 → 程序跟随变化 diff --git a/docs/PLAN.md b/docs/PLAN.md index f74b778..5c1988a 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -214,7 +214,7 @@ card_tags: (card_id, tag_id) 复合主键,双向 ON DELETE CASCADE 入口 = 顶部显示区域右侧"设置"按钮,承载各类全局设置: - 语言选择(英文 / 简体中文),切换后显示"重启后生效"提示(提示文本本身为资源键) -- 主题选择(亮色 / 暗色 / 跟随系统),切换后**即时生效**,无重启提示(与语言项的行为差异) +- 主题选择(亮色 / 暗色 / 跟随系统),下拉框选中即**即时生效**(预览),无重启提示(与语言项的行为差异);点确认才写入配置,取消或关闭设置模态则恢复打开时的主题 - 四类确认开关(见 §5.5) ### 5.7 关于模态