From 16a002189593c294850eefe4129553639124ea9b Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Sun, 28 Dec 2025 10:12:37 -0600 Subject: [PATCH 1/5] Add option to follow Windows theme Closes #83 --- DesktopClock/MainWindow.xaml | 6 +- DesktopClock/Properties/Settings.cs | 154 +++++++++++++++++++++++++++- DesktopClock/SettingsWindow.xaml | 97 ++++++++++-------- 3 files changed, 212 insertions(+), 45 deletions(-) diff --git a/DesktopClock/MainWindow.xaml b/DesktopClock/MainWindow.xaml index 32297ea..75001a9 100644 --- a/DesktopClock/MainWindow.xaml +++ b/DesktopClock/MainWindow.xaml @@ -111,7 +111,7 @@ + Color="{Binding EffectiveOuterColor, Source={x:Static p:Settings.Default}, Mode=OneWay}" /> @@ -126,7 +126,7 @@ + Color="{Binding EffectiveTextColor, Source={x:Static p:Settings.Default}, Mode=OneWay}" /> @@ -141,7 +141,7 @@ + Color="{Binding EffectiveOuterColor, Source={x:Static p:Settings.Default}, Mode=OneWay}" /> diff --git a/DesktopClock/Properties/Settings.cs b/DesktopClock/Properties/Settings.cs index 6c83d13..b729e60 100644 --- a/DesktopClock/Properties/Settings.cs +++ b/DesktopClock/Properties/Settings.cs @@ -1,7 +1,9 @@ using System; using System.ComponentModel; using System.IO; +using System.Windows; using System.Windows.Media; +using Microsoft.Win32; using Newtonsoft.Json; using WpfWindowPlacement; @@ -10,6 +12,11 @@ namespace DesktopClock.Properties; public sealed class Settings : INotifyPropertyChanged, IDisposable { private readonly FileSystemWatcher _watcher; + private bool _followSystemTheme; + private bool _systemThemeIsLight = true; + private Color _systemAccentColor = DefaultAccentColor; + private Color _textColor = Color.FromRgb(33, 33, 33); + private Color _outerColor = Color.FromRgb(247, 247, 247); private static readonly Lazy _default = new(LoadAndAttemptSave); @@ -25,6 +32,11 @@ public sealed class Settings : INotifyPropertyChanged, IDisposable public static readonly double MaxSizeLog = 6.5; public static readonly double MinSizeLog = 2.7; + private static readonly Color DefaultAccentColor = Color.FromRgb(0, 120, 215); + private static readonly Color LightThemeOuterColor = Color.FromRgb(247, 247, 247); + private static readonly Color DarkThemeOuterColor = Color.FromRgb(32, 32, 32); + private const string PersonalizeKeyPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + private const string AppsUseLightThemeValueName = "AppsUseLightTheme"; static Settings() { @@ -42,6 +54,10 @@ private Settings() EnableRaisingEvents = true, }; _watcher.Changed += FileChanged; + + RefreshSystemThemeColors(notifyIfFollowing: false); + SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged; + SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged; } #pragma warning disable CS0067 // The event 'Settings.PropertyChanged' is never used. Handled by Fody. @@ -122,7 +138,19 @@ private Settings() /// /// Text color for the clock's text. /// - public Color TextColor { get; set; } = Color.FromRgb(33, 33, 33); + public Color TextColor + { + get => _textColor; + set + { + if (_textColor.Equals(value)) + return; + + _textColor = value; + NotifyPropertyChanged(nameof(TextColor)); + NotifyEffectiveThemeChanged(); + } + } /// /// Opacity of the text. @@ -132,7 +160,51 @@ private Settings() /// /// The outer color, for either the background or the outline. /// - public Color OuterColor { get; set; } = Color.FromRgb(247, 247, 247); + public Color OuterColor + { + get => _outerColor; + set + { + if (_outerColor.Equals(value)) + return; + + _outerColor = value; + NotifyPropertyChanged(nameof(OuterColor)); + NotifyEffectiveThemeChanged(); + } + } + + /// + /// Follow the system theme and accent color. + /// + public bool FollowSystemTheme + { + get => _followSystemTheme; + set + { + if (_followSystemTheme == value) + return; + + _followSystemTheme = value; + NotifyPropertyChanged(nameof(FollowSystemTheme)); + RefreshSystemThemeColors(notifyIfFollowing: false); + NotifyEffectiveThemeChanged(); + } + } + + /// + /// Effective text color based on theme-following preference. + /// + [JsonIgnore] + public Color EffectiveTextColor => FollowSystemTheme ? _systemAccentColor : TextColor; + + /// + /// Effective outer color based on theme-following preference. + /// + [JsonIgnore] + public Color EffectiveOuterColor => FollowSystemTheme + ? (_systemThemeIsLight ? LightThemeOuterColor : DarkThemeOuterColor) + : OuterColor; /// /// Shows a solid background instead of an outline. @@ -382,6 +454,84 @@ public void ScaleHeight(double steps) Height = (int)exp; } + private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) + { + if (!FollowSystemTheme) + return; + + if (e.Category == UserPreferenceCategory.General || + e.Category == UserPreferenceCategory.Color || + e.Category == UserPreferenceCategory.VisualStyle) + { + RefreshSystemThemeColors(notifyIfFollowing: true); + } + } + + private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (!FollowSystemTheme) + return; + + if (e.PropertyName == nameof(SystemParameters.WindowGlassColor)) + { + RefreshSystemThemeColors(notifyIfFollowing: true); + } + } + + private void RefreshSystemThemeColors(bool notifyIfFollowing) + { + var dispatcher = Application.Current?.Dispatcher; + if (dispatcher != null && !dispatcher.CheckAccess()) + { + dispatcher.BeginInvoke(new Action(() => RefreshSystemThemeColors(notifyIfFollowing))); + return; + } + + var isLightTheme = GetSystemThemeIsLight(); + var accentColor = GetSystemAccentColor(); + var themeChanged = isLightTheme != _systemThemeIsLight; + var accentChanged = !_systemAccentColor.Equals(accentColor); + + _systemThemeIsLight = isLightTheme; + _systemAccentColor = accentColor; + + if (notifyIfFollowing && (themeChanged || accentChanged) && FollowSystemTheme) + { + NotifyEffectiveThemeChanged(); + } + } + + private static bool GetSystemThemeIsLight() + { + var value = Registry.GetValue(PersonalizeKeyPath, AppsUseLightThemeValueName, 1); + return value switch + { + int intValue => intValue > 0, + byte byteValue => byteValue > 0, + _ => true, + }; + } + + private static Color GetSystemAccentColor() + { + var accent = SystemParameters.WindowGlassColor; + if (accent.A == 0) + return DefaultAccentColor; + + return Color.FromArgb(255, accent.R, accent.G, accent.B); + } + + private void NotifyEffectiveThemeChanged() + { + NotifyPropertyChanged(nameof(EffectiveTextColor)); + NotifyPropertyChanged(nameof(EffectiveOuterColor)); + } + + private void NotifyPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + public void Dispose() { // We don't dispose of the watcher anymore because it would actually hang indefinitely if you had multiple instances of the same clock open. diff --git a/DesktopClock/SettingsWindow.xaml b/DesktopClock/SettingsWindow.xaml index 81766b2..ed472bc 100644 --- a/DesktopClock/SettingsWindow.xaml +++ b/DesktopClock/SettingsWindow.xaml @@ -44,6 +44,16 @@ + + - -