/ src / modules / launcher / PowerLauncher / Helper / ThemeHelper.cs
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  }