using System; using System.Linq; using System.Windows; using Microsoft.Win32; namespace TeamsISO.App.Services; /// /// Owns the active theme for the WPF host. Three preferences: /// /// System — follows the Windows app-mode setting (default for new /// users; re-reads on ). /// Dark — pin dark regardless of OS. /// Light — pin light regardless of OS. /// /// The two color files (Theme.Dark.xaml + Theme.Light.xaml) are /// kept in lockstep on the same set of brush keys; this manager swaps the /// MergedDictionaries entry at runtime. Styles + control templates in /// WildDragonTheme.xaml reach the brushes via , /// so the visual tree re-resolves without an app restart. /// /// Preference is persisted via 's Theme field, /// reserved on disk during the v1 → v2 rollout so the rebuild doesn't lose the /// operator's choice. /// public sealed class ThemeManager { public static ThemeManager Current { get; } = new(); private const string DarkUri = "/Themes/Theme.Dark.xaml"; private const string LightUri = "/Themes/Theme.Light.xaml"; private const string PreferenceKeySystem = "System"; private const string PreferenceKeyDark = "Dark"; private const string PreferenceKeyLight = "Light"; private ThemeManager() { // Hydrate preference from disk on first access. UIPreferences.Load() // is best-effort — disk failures fall back to defaults so the app // always boots into a deterministic theme. try { var prefs = UIPreferences.Load(); if (IsValidPreference(prefs.Theme)) { _preference = prefs.Theme; } } catch { // Defensive — singleton ctor must not throw or the app loses theming. } // Re-evaluate when Windows app-mode flips, but only when the // operator hasn't pinned a preference. The explicit choice wins. SystemEvents.UserPreferenceChanged += OnSystemPreferenceChanged; } private string _preference = PreferenceKeySystem; /// Current preference. One of "System", "Dark", "Light". public string Preference => _preference; /// Fires after a theme swap with the resolved (absolute) theme. public event EventHandler? Themed; /// /// Resolve the preference to an absolute theme name ("Dark" or "Light") /// suitable for the dictionary lookup. "System" resolves to the OS /// app-mode at the time of the call. /// public string ResolveTheme() => _preference switch { PreferenceKeyDark => PreferenceKeyDark, PreferenceKeyLight => PreferenceKeyLight, _ => IsSystemDark() ? PreferenceKeyDark : PreferenceKeyLight, }; /// /// Set the operator's preference, persist, and apply the resolved theme. /// public void Set(string preference) { if (!IsValidPreference(preference)) { throw new ArgumentException( "Preference must be 'System', 'Dark', or 'Light'.", nameof(preference)); } _preference = preference; try { UIPreferences.SetTheme(preference); } catch { /* persistence is best-effort */ } Apply(); } /// /// Cycle the theme between Dark and Light (one-click toggle from the header /// theme icon). If the current preference is "System", the cycle pins to /// the OPPOSITE of the currently-resolved theme so the click has a /// visible effect. /// public void Toggle() { var current = ResolveTheme(); Set(current == PreferenceKeyDark ? PreferenceKeyLight : PreferenceKeyDark); } /// /// Apply the current resolved theme. Should be called once during app /// startup (after Application.Current.Resources is initialized) and /// whenever changes — already /// does the latter for you. /// public void Apply() { var theme = ResolveTheme(); var uri = theme == PreferenceKeyDark ? DarkUri : LightUri; SwapColorDictionary(uri); Themed?.Invoke(this, theme); } private static void SwapColorDictionary(string newUri) { var app = Application.Current; if (app is null) return; var dicts = app.Resources.MergedDictionaries; // Find the existing theme color dictionary by source URI. We // distinguish "color" dictionaries from "WildDragonTheme" by name — // the color files are at Theme.Dark.xaml / Theme.Light.xaml; the // styles file is at WildDragonTheme.xaml. Replace in place to // preserve merge order so DynamicResource refs resolve to the new // brushes. ResourceDictionary? old = null; for (var i = 0; i < dicts.Count; i++) { var src = dicts[i].Source?.OriginalString ?? string.Empty; if (src.EndsWith("Theme.Dark.xaml", StringComparison.OrdinalIgnoreCase) || src.EndsWith("Theme.Light.xaml", StringComparison.OrdinalIgnoreCase)) { old = dicts[i]; break; } } var fresh = new ResourceDictionary { Source = new Uri(newUri, UriKind.Relative) }; if (old is null) { dicts.Insert(0, fresh); } else { var idx = dicts.IndexOf(old); dicts.RemoveAt(idx); dicts.Insert(idx, fresh); } } /// /// Read Windows' AppsUseLightTheme registry value. 1 = light, 0 = dark. /// Returns true (dark) on any read failure — the dark scene is the /// default per DESIGN.md so a missing value still lands somewhere sensible. /// private static bool IsSystemDark() { try { using var key = Registry.CurrentUser.OpenSubKey( @"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize"); if (key?.GetValue("AppsUseLightTheme") is int value) { return value == 0; } } catch { // Registry access can fail under unusual security contexts. } return true; } private void OnSystemPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e) { if (e.Category != UserPreferenceCategory.General) return; if (_preference != PreferenceKeySystem) return; // Marshal to the UI thread — registry events fire on a system pool // thread and resource dictionary mutations require dispatcher access. Application.Current?.Dispatcher.BeginInvoke(new Action(Apply)); } private static bool IsValidPreference(string? value) => value is PreferenceKeySystem or PreferenceKeyDark or PreferenceKeyLight; }