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:
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
@@ -30,7 +30,8 @@
|
|||||||
|
|
||||||
### A3 主题三态切换
|
### A3 主题三态切换
|
||||||
|
|
||||||
- [ ] 设置 → 主题 亮色 / 暗色 / 跟随系统 依次切换:**即时生效**,无重启提示
|
- [ ] 设置 → 主题 亮色 / 暗色 / 跟随系统 依次切换:下拉框一选中就**即时生效**(无需点确认),无重启提示
|
||||||
|
- [ ] 切换主题后点取消、按 Esc 或点标题栏关闭:恢复为打开设置时的主题;重启后仍是原主题
|
||||||
- [ ] 每种主题下检查:主窗口、看板列与卡片、标签 tab、全部模态、全部错误页颜色完整切换,无亮底暗字/暗底亮字残留
|
- [ ] 每种主题下检查:主窗口、看板列与卡片、标签 tab、全部模态、全部错误页颜色完整切换,无亮底暗字/暗底亮字残留
|
||||||
- [ ] 标签徽章在两种主题下均显示原色(§5.9 例外),文字颜色保持可读
|
- [ ] 标签徽章在两种主题下均显示原色(§5.9 例外),文字颜色保持可读
|
||||||
- [ ] 跟随系统:程序运行中切换 Windows 深浅色 → 程序跟随变化
|
- [ ] 跟随系统:程序运行中切换 Windows 深浅色 → 程序跟随变化
|
||||||
|
|||||||
+1
-1
@@ -214,7 +214,7 @@ card_tags: (card_id, tag_id) 复合主键,双向 ON DELETE CASCADE
|
|||||||
入口 = 顶部显示区域右侧"设置"按钮,承载各类全局设置:
|
入口 = 顶部显示区域右侧"设置"按钮,承载各类全局设置:
|
||||||
|
|
||||||
- 语言选择(英文 / 简体中文),切换后显示"重启后生效"提示(提示文本本身为资源键)
|
- 语言选择(英文 / 简体中文),切换后显示"重启后生效"提示(提示文本本身为资源键)
|
||||||
- 主题选择(亮色 / 暗色 / 跟随系统),切换后**即时生效**,无重启提示(与语言项的行为差异)
|
- 主题选择(亮色 / 暗色 / 跟随系统),下拉框选中即**即时生效**(预览),无重启提示(与语言项的行为差异);点确认才写入配置,取消或关闭设置模态则恢复打开时的主题
|
||||||
- 四类确认开关(见 §5.5)
|
- 四类确认开关(见 §5.5)
|
||||||
|
|
||||||
### 5.7 关于模态
|
### 5.7 关于模态
|
||||||
|
|||||||
Reference in new issue
Block a user