ThemeHelper.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.Globalization; 8 using System.IO; 9 using System.Runtime.CompilerServices; 10 using ManagedCommon; 11 using PowerLauncher.Services; 12 13 [assembly: InternalsVisibleTo("Wox.Test")] 14 15 namespace PowerLauncher.Helper; 16 17 /// <summary> 18 /// Provides functionality for determining the application's theme based on system settings, user 19 /// preferences, and High Contrast mode detection. 20 /// </summary> 21 public class ThemeHelper 22 { 23 private const string ThemesKey = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes"; 24 private const string PersonalizeKey = ThemesKey + "\\Personalize"; 25 26 internal const int AppsUseLightThemeLight = 1; 27 internal const int AppsUseLightThemeDark = 0; 28 29 /// <summary> 30 /// Default value for the "AppsUseLightTheme" registry setting. This value represents Light 31 /// mode and will be used if the registry value is invalid or cannot be read. 32 /// </summary> 33 internal const int AppsUseLightThemeDefault = AppsUseLightThemeLight; 34 35 private readonly IRegistryService _registryService; 36 37 private readonly Dictionary<string, Theme> _highContrastThemeMap = 38 new(StringComparer.InvariantCultureIgnoreCase) 39 { 40 { "hc1", Theme.HighContrastOne }, 41 { "hc2", Theme.HighContrastTwo }, 42 { "hcwhite", Theme.HighContrastWhite }, 43 { "hcblack", Theme.HighContrastBlack }, 44 }; 45 46 /// <summary> 47 /// Initializes a new instance of the <see cref="ThemeHelper"/> class. 48 /// </summary> 49 /// <param name="registryService">The service used to query registry values. If <c>null</c>, a 50 /// default implementation is used, which queries the Windows registry. This allows for 51 /// dependency injection and unit testing.</param> 52 public ThemeHelper(IRegistryService registryService = null) 53 { 54 _registryService = registryService ?? RegistryServiceFactory.Create(); 55 } 56 57 /// <summary> 58 /// Determines the theme to apply, prioritizing an active High Contrast theme. 59 /// </summary> 60 /// <param name="settingsTheme">The theme selected in application settings.</param> 61 /// <returns>The resolved <see cref="Theme"/> based on the following priority order: 62 /// 1. If a default High Contrast Windows theme is active, return the corresponding High 63 /// Contrast <see cref="Theme"/>. 64 /// 2. If "Windows default" is selected in application settings, return the Windows app theme 65 /// (<see cref="Theme.Dark"/> or <see cref="Theme.Light"/>). 66 /// 3. If the user explicitly selected "Light" or "Dark", return their chosen theme. 67 /// 4. If the theme cannot be determined, return <see cref="Theme.Light"/>. 68 /// </returns> 69 public Theme DetermineTheme(Theme settingsTheme) => 70 GetHighContrastTheme() ?? 71 (settingsTheme == Theme.System ? GetAppsTheme() : ValidateTheme(settingsTheme)); 72 73 /// <summary> 74 /// Ensures the provided <see cref="Theme"/> value is valid. 75 /// </summary> 76 /// <param name="theme">The <see cref="Theme"/> value to validate.</param> 77 /// <returns>The provided theme if it is a defined enum value; otherwise, defaults to 78 /// <see cref="Theme.Light"/>. 79 private Theme ValidateTheme(Theme theme) => Enum.IsDefined(theme) ? theme : Theme.Light; 80 81 /// <summary> 82 /// Determines if a High Contrast theme is currently active and returns the corresponding 83 /// <see cref="Theme"/>. 84 /// </summary> 85 /// <returns>The detected High Contrast <see cref="Theme"/> (e.g. 86 /// <see cref="Theme.HighContrastOne"/>, or <c>null</c> if no recognized High Contrast theme 87 /// is active. 88 /// </returns> 89 internal Theme? GetHighContrastTheme() 90 { 91 try 92 { 93 var themePath = Convert.ToString( 94 _registryService.GetValue(ThemesKey, "CurrentTheme", string.Empty), 95 CultureInfo.InvariantCulture); 96 97 if (!string.IsNullOrEmpty(themePath) && _highContrastThemeMap.TryGetValue( 98 Path.GetFileNameWithoutExtension(themePath), out var theme)) 99 { 100 return theme; 101 } 102 } 103 catch 104 { 105 // Fall through to return null. Ignore exception. 106 } 107 108 return null; 109 } 110 111 /// <summary> 112 /// Retrieves the Windows app theme preference from the registry. 113 /// </summary> 114 /// <returns><see cref="Theme.Dark"/> if the user has selected Dark mode for apps, 115 /// <see cref="Theme.Light"/> otherwise. If the registry value cannot be read or is invalid, 116 /// the default value (<see cref="Theme.Light"/>) is used. 117 /// </returns> 118 internal Theme GetAppsTheme() 119 { 120 try 121 { 122 // "AppsUseLightTheme" registry value: 123 // - 0 = Dark mode 124 // - 1 (or missing/invalid) = Light mode 125 var regValue = _registryService.GetValue( 126 PersonalizeKey, 127 "AppsUseLightTheme", 128 AppsUseLightThemeDefault); 129 130 return regValue is int intValue && intValue == 0 ? Theme.Dark : Theme.Light; 131 } 132 catch 133 { 134 return Theme.Light; 135 } 136 } 137 }