using System.ComponentModel; using System.Windows; using System.Windows.Interop; using CursorLang.Core.Models; using CursorLang.Settings.Interop; using Microsoft.Win32; namespace CursorLang.Settings.Services; /// /// Keeps the palette of the chosen theme in the application resources and swaps it /// when the setting changes — the windows are recoloured without a restart. /// public sealed class ThemeService : IThemeService, IDisposable { private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; private readonly AppSettings _settings; private readonly Func _detectSystemTheme; private readonly List _windows = []; private ResourceDictionary? _palette; private AppTheme _current; public ThemeService(AppSettings settings) : this(settings, DetectSystemTheme) { } /// /// Takes the source of the system theme explicitly: in tests it is not the registry /// that provides it. /// internal ThemeService(AppSettings settings, Func detectSystemTheme) { _settings = settings; _detectSystemTheme = detectSystemTheme; _settings.PropertyChanged += OnSettingsChanged; SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged; Apply(); } /// The theme the user sees: System is already resolved here. public AppTheme CurrentTheme => _current; public void Register(Window window) { _windows.Add(window); window.Closed += OnWindowClosed; if (new WindowInteropHelper(window).Handle == IntPtr.Zero) { window.SourceInitialized += OnWindowSourceInitialized; return; } ApplyTitleBar(window); } public void Dispose() { _settings.PropertyChanged -= OnSettingsChanged; SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged; foreach (Window window in _windows) { window.Closed -= OnWindowClosed; window.SourceInitialized -= OnWindowSourceInitialized; } _windows.Clear(); } /// The app theme from the Windows settings. internal static AppTheme DetectSystemTheme() { try { using RegistryKey? key = Registry.CurrentUser.OpenSubKey(PersonalizeKey); return key?.GetValue("AppsUseLightTheme") is int light && light == 0 ? AppTheme.Dark : AppTheme.Light; } catch (Exception e) when (e is System.Security.SecurityException or UnauthorizedAccessException) { return AppTheme.Light; } } private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(AppSettings.Theme)) { Apply(); } } // Windows reports a theme change from outside the interface thread private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e) => Application.Current?.Dispatcher.InvokeAsync(Apply); private void Apply() { AppTheme theme = _settings.Theme == AppTheme.System ? _detectSystemTheme() : _settings.Theme; if (_palette is not null && theme == _current) { return; } _current = theme; var next = new ResourceDictionary { Source = PaletteUri(theme), }; ICollection dictionaries = Application.Current.Resources.MergedDictionaries; dictionaries.Add(next); if (_palette is not null) { dictionaries.Remove(_palette); } _palette = next; foreach (Window window in _windows) { ApplyTitleBar(window); } } // The assembly name in the address is there on purpose: without it the dictionary // is looked up in the assembly the process started from, which is not always the // one holding the palettes. It is taken from the type rather than spelt out — // the name of this assembly has changed once already, and a wrong one here is a // crash on startup rather than a build error internal static Uri PaletteUri(AppTheme theme) => new( $"pack://application:,,,/{typeof(ThemeService).Assembly.GetName().Name};component/Themes/{theme}.xaml", UriKind.Absolute); private void ApplyTitleBar(Window window) => WindowThemeNative.SetDarkTitleBar( new WindowInteropHelper(window).Handle, _current == AppTheme.Dark); private void OnWindowSourceInitialized(object? sender, EventArgs e) { if (sender is Window window) { window.SourceInitialized -= OnWindowSourceInitialized; ApplyTitleBar(window); } } private void OnWindowClosed(object? sender, EventArgs e) { if (sender is Window window) { window.Closed -= OnWindowClosed; _windows.Remove(window); } } }