diff --git a/DesktopClock/Properties/Settings.cs b/DesktopClock/Properties/Settings.cs index 6c83d13..cf11630 100644 --- a/DesktopClock/Properties/Settings.cs +++ b/DesktopClock/Properties/Settings.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.IO; using System.Windows.Media; +using DesktopClock.Utilities; using Newtonsoft.Json; using WpfWindowPlacement; @@ -25,7 +26,6 @@ public sealed class Settings : INotifyPropertyChanged, IDisposable public static readonly double MaxSizeLog = 6.5; public static readonly double MinSizeLog = 2.7; - static Settings() { // Settings file path from the same directory as the executable. @@ -349,6 +349,11 @@ private static Settings LoadAndAttemptSave() { var settings = LoadFromFile(); + if (!File.Exists(FilePath)) + { + settings.ApplySystemThemeDefaultsIfAvailable(); + } + CanBeSaved = settings.Save(); return settings; @@ -382,6 +387,15 @@ public void ScaleHeight(double steps) Height = (int)exp; } + private void ApplySystemThemeDefaultsIfAvailable() + { + if (!SystemThemeService.TryGetThemeDefaults(out var textColor, out var outerColor)) + return; + + TextColor = textColor; + OuterColor = outerColor; + } + 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/Utilities/SystemThemeService.cs b/DesktopClock/Utilities/SystemThemeService.cs new file mode 100644 index 0000000..26203da --- /dev/null +++ b/DesktopClock/Utilities/SystemThemeService.cs @@ -0,0 +1,172 @@ +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Media; +using Microsoft.Win32; + +namespace DesktopClock.Utilities; + +/// +/// Reads Windows theme and accent color values to seed default UI colors. +/// +public static class SystemThemeService +{ + // Fallbacks match the app's existing default colors. + 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); + + // Theme and colorization values stored under HKCU. + private const string PersonalizeKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + private const string DwmKeyPath = @"Software\Microsoft\Windows\DWM"; + private const string AppsUseLightThemeValueName = "AppsUseLightTheme"; + private const string SystemUsesLightThemeValueName = "SystemUsesLightTheme"; + private const string ColorizationColorValueName = "ColorizationColor"; + + /// + /// Tries to build default text/outer colors from the system theme and accent color. + /// + /// + /// Returns false when the system theme cannot be read (older Windows or missing keys). + /// + public static bool TryGetThemeDefaults(out Color textColor, out Color outerColor) + { + textColor = default; + outerColor = default; + + if (!TryGetSystemThemeIsLight(out var isLightTheme)) + return false; + + textColor = GetSystemAccentColor(); + outerColor = isLightTheme ? LightThemeOuterColor : DarkThemeOuterColor; + return true; + } + + /// + /// Reads the light/dark preference from the Windows personalize key. + /// + private static bool TryGetSystemThemeIsLight(out bool isLightTheme) + { + // AppsUseLightTheme is the primary indicator for Win10+ app theme. + if (TryGetRegistryDword(PersonalizeKeyPath, AppsUseLightThemeValueName, out var appsUseLightTheme)) + { + isLightTheme = appsUseLightTheme > 0; + return true; + } + + // SystemUsesLightTheme is a fallback when AppsUseLightTheme is absent. + if (TryGetRegistryDword(PersonalizeKeyPath, SystemUsesLightThemeValueName, out var systemUsesLightTheme)) + { + isLightTheme = systemUsesLightTheme > 0; + return true; + } + + // Unknown; caller treats this as "no system theme available". + isLightTheme = true; + return false; + } + + /// + /// Resolves the current accent color using DWM, registry, then system parameters. + /// + private static Color GetSystemAccentColor() + { + // Prefer DWM because it tracks the active colorization value. + if (TryGetAccentColorFromDwm(out var accent) || TryGetAccentColorFromRegistry(out accent)) + return accent; + + // SystemParameters is a safe fallback when DWM/registry are unavailable. + accent = SystemParameters.WindowGlassColor; + if (accent.A != 0) + return Color.FromArgb(255, accent.R, accent.G, accent.B); + + return DefaultAccentColor; + } + + /// + /// Reads a DWORD value from HKCU, if present. + /// + private static bool TryGetRegistryDword(string keyPath, string valueName, out int value) + { + value = default; + + try + { + using var key = Registry.CurrentUser.OpenSubKey(keyPath); + if (key?.GetValue(valueName) is int intValue) + { + value = intValue; + return true; + } + + if (key?.GetValue(valueName) is byte byteValue) + { + value = byteValue; + return true; + } + + if (key?.GetValue(valueName) is uint uintValue) + { + value = unchecked((int)uintValue); + return true; + } + } + catch + { + } + + return false; + } + + /// + /// Uses DWM to fetch the current colorization color. + /// + private static bool TryGetAccentColorFromDwm(out Color color) + { + color = default; + + try + { + var result = DwmGetColorizationColor(out var colorizationColor, out _); + if (result != 0) + return false; + + color = ColorFromArgbUint(colorizationColor); + return true; + } + catch + { + return false; + } + } + + /// + /// Reads the colorization color from the DWM registry key. + /// + private static bool TryGetAccentColorFromRegistry(out Color color) + { + color = default; + + if (!TryGetRegistryDword(DwmKeyPath, ColorizationColorValueName, out var colorization)) + return false; + + color = ColorFromArgbUint(unchecked((uint)colorization)); + return true; + } + + /// + /// Converts a DWM colorization ARGB value into an opaque WPF color. + /// + private static Color ColorFromArgbUint(uint color) + { + var r = (byte)((color >> 16) & 0xFF); + var g = (byte)((color >> 8) & 0xFF); + var b = (byte)(color & 0xFF); + return Color.FromArgb(255, r, g, b); + } + + /// + /// Win32 API for reading the current DWM colorization color. + /// + [DllImport("dwmapi.dll", PreserveSig = true)] + private static extern int DwmGetColorizationColor(out uint colorizationColor, out bool opaqueBlend); +}