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.
This commit is contained in:
doyaGu committed 2026-10-03 09:12:36 -04:00
1 parent 1ecb48316d
commit 4a88033695
5 files changed
+109 -7

No files matched your search

@@ -0,0 +1,63 @@
using YKanBan.Storage.AppData;
using YKanBan.ViewModels.Dialogs;
namespace YKanBan.Tests.ViewModels;
/// <summary>
/// Tests for the settings dialog: the theme is previewed as soon as it is
/// selected, saved on accept and restored on cancel.
/// </summary>
[TestClass]
public class SettingsDialogViewModelTests
{
[TestMethod]
public void OpeningTheDialogAppliesNothing()
{
var applied = new List<ThemeOption>();
_ = 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<ThemeOption>();
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<ThemeOption>();
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<ThemeOption>();
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);
}
}
@@ -5,23 +5,27 @@ namespace YKanBan.ViewModels.Dialogs;
/// <summary> /// <summary>
/// The settings dialog: language (restart to apply), theme and the four /// The settings dialog: language (restart to apply), theme and the four
/// confirmation-dialog toggles. Nothing is applied until the user accepts; /// confirmation-dialog toggles. A theme selection is previewed immediately;
/// cancelling or closing the window discards the changes. /// everything else waits until the user accepts. Cancelling or closing the
/// window discards the changes and restores the theme the dialog opened with.
/// </summary> /// </summary>
public sealed partial class SettingsDialogViewModel : ViewModelBase public sealed partial class SettingsDialogViewModel : ViewModelBase
{ {
private readonly AppConfig _config; private readonly AppConfig _config;
private readonly string _startupLanguage; private readonly string _startupLanguage;
private readonly Action<ThemeOption> _applyTheme;
/// <summary> /// <summary>
/// Initializes the dialog from the live configuration. /// Initializes the dialog from the live configuration.
/// </summary> /// </summary>
/// <param name="config">The live application configuration.</param> /// <param name="config">The live application configuration.</param>
/// <param name="startupLanguage">The language applied at startup, for the restart notice.</param> /// <param name="startupLanguage">The language applied at startup, for the restart notice.</param>
public SettingsDialogViewModel(AppConfig config, string startupLanguage) /// <param name="applyTheme">Applies a theme to the running app; defaults to <see cref="App.ApplyTheme"/>.</param>
public SettingsDialogViewModel(AppConfig config, string startupLanguage, Action<ThemeOption>? applyTheme = null)
{ {
_config = config; _config = config;
_startupLanguage = startupLanguage; _startupLanguage = startupLanguage;
_applyTheme = applyTheme ?? App.ApplyTheme;
Languages = Languages =
[ [
@@ -63,7 +67,8 @@ public sealed partial class SettingsDialogViewModel : ViewModelBase
private OptionItem? selectedLanguage; private OptionItem? selectedLanguage;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty]
private OptionItem? selectedTheme; private OptionItem? selectedTheme;
@@ -110,7 +115,6 @@ public sealed partial class SettingsDialogViewModel : ViewModelBase
if (SelectedTheme is not null) if (SelectedTheme is not null)
{ {
_config.Theme = (ThemeOption)SelectedTheme.Value; _config.Theme = (ThemeOption)SelectedTheme.Value;
App.ApplyTheme(_config.Theme);
} }
_config.Confirmations.DeleteCard = ConfirmDeleteCard; _config.Confirmations.DeleteCard = ConfirmDeleteCard;
@@ -119,6 +123,23 @@ public sealed partial class SettingsDialogViewModel : ViewModelBase
_config.Confirmations.DiscardEdit = ConfirmDiscardEdit; _config.Confirmations.DiscardEdit = ConfirmDiscardEdit;
} }
/// <summary>
/// Discards the working values and restores the theme the dialog opened with.
/// </summary>
public void Cancel() => _applyTheme(_config.Theme);
/// <summary>
/// Previews a theme selection on the running app.
/// </summary>
/// <param name="value">The new selection.</param>
partial void OnSelectedThemeChanged(OptionItem? value)
{
if (value is not null)
{
_applyTheme((ThemeOption)value.Value);
}
}
/// <summary> /// <summary>
/// Refreshes the restart notice after a language selection change. /// Refreshes the restart notice after a language selection change.
/// </summary> /// </summary>
@@ -6,6 +6,8 @@ namespace YKanBan.Views.Dialogs;
public partial class SettingsDialog : Window public partial class SettingsDialog : Window
{ {
private bool _accepted;
public SettingsDialog() public SettingsDialog()
{ {
InitializeComponent(); InitializeComponent();
@@ -15,9 +17,24 @@ public partial class SettingsDialog : Window
private void OnOkClick(object? sender, RoutedEventArgs e) private void OnOkClick(object? sender, RoutedEventArgs e)
{ {
(DataContext as SettingsDialogViewModel)?.Accept(); (DataContext as SettingsDialogViewModel)?.Accept();
_accepted = true;
Close(true); Close(true);
} }
/// <summary>Closes without applying anything.</summary> /// <summary>Closes without applying anything.</summary>
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false); private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false);
/// <summary>
/// Restores the previewed theme on every close that did not accept,
/// including the title-bar close button and Escape.
/// </summary>
/// <param name="e">The event arguments.</param>
protected override void OnClosed(EventArgs e)
{
if (!_accepted)
{
(DataContext as SettingsDialogViewModel)?.Cancel();
}
base.OnClosed(e);
}
} }
+2 -1
View File
@@ -30,7 +30,8 @@
### A3 主题三态切换 ### A3 主题三态切换
- [ ] 设置 → 主题 亮色 / 暗色 / 跟随系统 依次切换:**即时生效**,无重启提示 - [ ] 设置 → 主题 亮色 / 暗色 / 跟随系统 依次切换:下拉框一选中就**即时生效**(无需点确认),无重启提示
- [ ] 切换主题后点取消、按 Esc 或点标题栏关闭:恢复为打开设置时的主题;重启后仍是原主题
- [ ] 每种主题下检查:主窗口、看板列与卡片、标签 tab、全部模态、全部错误页颜色完整切换,无亮底暗字/暗底亮字残留 - [ ] 每种主题下检查:主窗口、看板列与卡片、标签 tab、全部模态、全部错误页颜色完整切换,无亮底暗字/暗底亮字残留
- [ ] 标签徽章在两种主题下均显示原色(§5.9 例外),文字颜色保持可读 - [ ] 标签徽章在两种主题下均显示原色(§5.9 例外),文字颜色保持可读
- [ ] 跟随系统:程序运行中切换 Windows 深浅色 → 程序跟随变化 - [ ] 跟随系统:程序运行中切换 Windows 深浅色 → 程序跟随变化
+1 -1
View File
@@ -214,7 +214,7 @@ card_tags: (card_id, tag_id) 复合主键,双向 ON DELETE CASCADE
入口 = 顶部显示区域右侧"设置"按钮,承载各类全局设置: 入口 = 顶部显示区域右侧"设置"按钮,承载各类全局设置:
- 语言选择(英文 / 简体中文),切换后显示"重启后生效"提示(提示文本本身为资源键) - 语言选择(英文 / 简体中文),切换后显示"重启后生效"提示(提示文本本身为资源键)
- 主题选择(亮色 / 暗色 / 跟随系统),切换后**即时生效**,无重启提示(与语言项的行为差异) - 主题选择(亮色 / 暗色 / 跟随系统),下拉框选中即**即时生效**(预览),无重启提示(与语言项的行为差异);点确认才写入配置,取消或关闭设置模态则恢复打开时的主题
- 四类确认开关(见 §5.5) - 四类确认开关(见 §5.5)
### 5.7 关于模态 ### 5.7 关于模态