mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
[Run] Fix dark mode detection code, plus refactor (#37324)
* Fix risky int cast in dark mode detection. * Refactored Helper and Manager classes. New unit tests and changes to support Registry access mocking. * Spelling update. * Improve documentation for the registry-related classes. * Fix issue with UpdateTheme raised in review. Enhance documentation. Rewrite tests to use parameterised unit tests, and expand to cover more cases.
This commit is contained in:
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace PowerLauncher.Helper
|
||||
{
|
||||
public static class ThemeExtensions
|
||||
{
|
||||
public static Theme GetCurrentTheme()
|
||||
{
|
||||
// Check for high-contrast mode
|
||||
Theme highContrastTheme = GetHighContrastBaseType();
|
||||
if (highContrastTheme != Theme.Light)
|
||||
{
|
||||
return highContrastTheme;
|
||||
}
|
||||
|
||||
// Check if the system is using dark or light mode
|
||||
return IsSystemDarkMode() ? Theme.Dark : Theme.Light;
|
||||
}
|
||||
|
||||
private static bool IsSystemDarkMode()
|
||||
{
|
||||
const string registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
|
||||
const string registryValue = "AppsUseLightTheme";
|
||||
|
||||
// Retrieve the registry value, which is a DWORD (0 or 1)
|
||||
object registryValueObj = Registry.GetValue(registryKey, registryValue, null);
|
||||
if (registryValueObj != null)
|
||||
{
|
||||
// 0 = Dark mode, 1 = Light mode
|
||||
bool isLightMode = Convert.ToBoolean((int)registryValueObj, CultureInfo.InvariantCulture);
|
||||
return !isLightMode; // Invert because 0 = Dark
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to Light theme if the registry key is missing
|
||||
return false; // Default to dark mode assumption
|
||||
}
|
||||
}
|
||||
|
||||
public static Theme GetHighContrastBaseType()
|
||||
{
|
||||
const string registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes";
|
||||
const string registryValue = "CurrentTheme";
|
||||
|
||||
string themePath = (string)Registry.GetValue(registryKey, registryValue, string.Empty);
|
||||
if (string.IsNullOrEmpty(themePath))
|
||||
{
|
||||
return Theme.Light; // Default to light theme if missing
|
||||
}
|
||||
|
||||
string theme = themePath.Split('\\').Last().Split('.').First().ToLowerInvariant();
|
||||
|
||||
return theme switch
|
||||
{
|
||||
"hc1" => Theme.HighContrastOne,
|
||||
"hc2" => Theme.HighContrastTwo,
|
||||
"hcwhite" => Theme.HighContrastWhite,
|
||||
"hcblack" => Theme.HighContrastBlack,
|
||||
_ => Theme.Light,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/modules/launcher/PowerLauncher/Helper/ThemeHelper.cs
Normal file
137
src/modules/launcher/PowerLauncher/Helper/ThemeHelper.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using ManagedCommon;
|
||||
using PowerLauncher.Services;
|
||||
|
||||
[assembly: InternalsVisibleTo("Wox.Test")]
|
||||
|
||||
namespace PowerLauncher.Helper;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality for determining the application's theme based on system settings, user
|
||||
/// preferences, and High Contrast mode detection.
|
||||
/// </summary>
|
||||
public class ThemeHelper
|
||||
{
|
||||
private const string ThemesKey = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes";
|
||||
private const string PersonalizeKey = ThemesKey + "\\Personalize";
|
||||
|
||||
internal const int AppsUseLightThemeLight = 1;
|
||||
internal const int AppsUseLightThemeDark = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Default value for the "AppsUseLightTheme" registry setting. This value represents Light
|
||||
/// mode and will be used if the registry value is invalid or cannot be read.
|
||||
/// </summary>
|
||||
internal const int AppsUseLightThemeDefault = AppsUseLightThemeLight;
|
||||
|
||||
private readonly IRegistryService _registryService;
|
||||
|
||||
private readonly Dictionary<string, Theme> _highContrastThemeMap =
|
||||
new(StringComparer.InvariantCultureIgnoreCase)
|
||||
{
|
||||
{ "hc1", Theme.HighContrastOne },
|
||||
{ "hc2", Theme.HighContrastTwo },
|
||||
{ "hcwhite", Theme.HighContrastWhite },
|
||||
{ "hcblack", Theme.HighContrastBlack },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ThemeHelper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="registryService">The service used to query registry values. If <c>null</c>, a
|
||||
/// default implementation is used, which queries the Windows registry. This allows for
|
||||
/// dependency injection and unit testing.</param>
|
||||
public ThemeHelper(IRegistryService registryService = null)
|
||||
{
|
||||
_registryService = registryService ?? RegistryServiceFactory.Create();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the theme to apply, prioritizing an active High Contrast theme.
|
||||
/// </summary>
|
||||
/// <param name="settingsTheme">The theme selected in application settings.</param>
|
||||
/// <returns>The resolved <see cref="Theme"/> based on the following priority order:
|
||||
/// 1. If a default High Contrast Windows theme is active, return the corresponding High
|
||||
/// Contrast <see cref="Theme"/>.
|
||||
/// 2. If "Windows default" is selected in application settings, return the Windows app theme
|
||||
/// (<see cref="Theme.Dark"/> or <see cref="Theme.Light"/>).
|
||||
/// 3. If the user explicitly selected "Light" or "Dark", return their chosen theme.
|
||||
/// 4. If the theme cannot be determined, return <see cref="Theme.Light"/>.
|
||||
/// </returns>
|
||||
public Theme DetermineTheme(Theme settingsTheme) =>
|
||||
GetHighContrastTheme() ??
|
||||
(settingsTheme == Theme.System ? GetAppsTheme() : ValidateTheme(settingsTheme));
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the provided <see cref="Theme"/> value is valid.
|
||||
/// </summary>
|
||||
/// <param name="theme">The <see cref="Theme"/> value to validate.</param>
|
||||
/// <returns>The provided theme if it is a defined enum value; otherwise, defaults to
|
||||
/// <see cref="Theme.Light"/>.
|
||||
private Theme ValidateTheme(Theme theme) => Enum.IsDefined(theme) ? theme : Theme.Light;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a High Contrast theme is currently active and returns the corresponding
|
||||
/// <see cref="Theme"/>.
|
||||
/// </summary>
|
||||
/// <returns>The detected High Contrast <see cref="Theme"/> (e.g.
|
||||
/// <see cref="Theme.HighContrastOne"/>, or <c>null</c> if no recognized High Contrast theme
|
||||
/// is active.
|
||||
/// </returns>
|
||||
internal Theme? GetHighContrastTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
var themePath = Convert.ToString(
|
||||
_registryService.GetValue(ThemesKey, "CurrentTheme", string.Empty),
|
||||
CultureInfo.InvariantCulture);
|
||||
|
||||
if (!string.IsNullOrEmpty(themePath) && _highContrastThemeMap.TryGetValue(
|
||||
Path.GetFileNameWithoutExtension(themePath), out var theme))
|
||||
{
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to return null. Ignore exception.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Windows app theme preference from the registry.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Theme.Dark"/> if the user has selected Dark mode for apps,
|
||||
/// <see cref="Theme.Light"/> otherwise. If the registry value cannot be read or is invalid,
|
||||
/// the default value (<see cref="Theme.Light"/>) is used.
|
||||
/// </returns>
|
||||
internal Theme GetAppsTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
// "AppsUseLightTheme" registry value:
|
||||
// - 0 = Dark mode
|
||||
// - 1 (or missing/invalid) = Light mode
|
||||
var regValue = _registryService.GetValue(
|
||||
PersonalizeKey,
|
||||
"AppsUseLightTheme",
|
||||
AppsUseLightThemeDefault);
|
||||
|
||||
return regValue is int intValue && intValue == 0 ? Theme.Dark : Theme.Light;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Theme.Light;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using ManagedCommon;
|
||||
@@ -17,10 +17,11 @@ namespace PowerLauncher.Helper
|
||||
{
|
||||
private readonly PowerToysRunSettings _settings;
|
||||
private readonly MainWindow _mainWindow;
|
||||
private ManagedCommon.Theme _currentTheme;
|
||||
private readonly ThemeHelper _themeHelper = new();
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public ManagedCommon.Theme CurrentTheme => _currentTheme;
|
||||
public Theme CurrentTheme { get; private set; }
|
||||
|
||||
public event Common.UI.ThemeChangedHandler ThemeChanged;
|
||||
|
||||
@@ -40,23 +41,25 @@ namespace PowerLauncher.Helper
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSystemTheme(ManagedCommon.Theme theme)
|
||||
private void SetSystemTheme(Theme theme)
|
||||
{
|
||||
_mainWindow.Background = OSVersionHelper.IsWindows11() is false ? SystemColors.WindowBrush : null;
|
||||
_mainWindow.Background = !OSVersionHelper.IsWindows11() ? SystemColors.WindowBrush : null;
|
||||
|
||||
// Need to disable WPF0001 since setting Application.Current.ThemeMode is experimental
|
||||
// https://learn.microsoft.com/en-us/dotnet/desktop/wpf/whats-new/net90#set-in-code
|
||||
#pragma warning disable WPF0001
|
||||
Application.Current.ThemeMode = theme is ManagedCommon.Theme.Light ? ThemeMode.Light : ThemeMode.Dark;
|
||||
if (theme is ManagedCommon.Theme.Dark or ManagedCommon.Theme.Light)
|
||||
Application.Current.ThemeMode = theme == Theme.Light ? ThemeMode.Light : ThemeMode.Dark;
|
||||
#pragma warning restore WPF0001
|
||||
|
||||
if (theme is Theme.Dark or Theme.Light)
|
||||
{
|
||||
if (!OSVersionHelper.IsWindows11())
|
||||
{
|
||||
// Apply background only on Windows 10
|
||||
// Windows theme does not work properly for dark and light mode so right now set the background color manual.
|
||||
// Windows theme does not work properly for dark and light mode so right now set the background color manually.
|
||||
_mainWindow.Background = new SolidColorBrush
|
||||
{
|
||||
Color = theme is ManagedCommon.Theme.Dark ? (Color)ColorConverter.ConvertFromString("#202020") : (Color)ColorConverter.ConvertFromString("#fafafa"),
|
||||
Color = (Color)ColorConverter.ConvertFromString(theme == Theme.Dark ? "#202020" : "#fafafa"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -64,49 +67,46 @@ namespace PowerLauncher.Helper
|
||||
{
|
||||
string styleThemeString = theme switch
|
||||
{
|
||||
ManagedCommon.Theme.Light => "Themes/Light.xaml",
|
||||
ManagedCommon.Theme.Dark => "Themes/Dark.xaml",
|
||||
ManagedCommon.Theme.HighContrastOne => "Themes/HighContrast1.xaml",
|
||||
ManagedCommon.Theme.HighContrastTwo => "Themes/HighContrast2.xaml",
|
||||
ManagedCommon.Theme.HighContrastWhite => "Themes/HighContrastWhite.xaml",
|
||||
_ => "Themes/HighContrastBlack.xaml",
|
||||
Theme.HighContrastOne => "Themes/HighContrast1.xaml",
|
||||
Theme.HighContrastTwo => "Themes/HighContrast2.xaml",
|
||||
Theme.HighContrastWhite => "Themes/HighContrastWhite.xaml",
|
||||
Theme.HighContrastBlack => "Themes/HighContrastBlack.xaml",
|
||||
_ => "Themes/Light.xaml",
|
||||
};
|
||||
|
||||
_mainWindow.Resources.MergedDictionaries.Clear();
|
||||
_mainWindow.Resources.MergedDictionaries.Add(new ResourceDictionary
|
||||
{
|
||||
Source = new Uri(styleThemeString, UriKind.Relative),
|
||||
});
|
||||
ResourceDictionary test = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri(styleThemeString, UriKind.Relative),
|
||||
};
|
||||
|
||||
if (OSVersionHelper.IsWindows11())
|
||||
{
|
||||
// Apply background only on Windows 11 to keep the same style as WPFUI
|
||||
_mainWindow.Background = new SolidColorBrush
|
||||
{
|
||||
Color = (Color)_mainWindow.FindResource("LauncherBackgroundColor"), // Use your DynamicResource key here
|
||||
Color = (Color)_mainWindow.FindResource("LauncherBackgroundColor"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ImageLoader.UpdateIconPath(theme);
|
||||
ThemeChanged?.Invoke(_currentTheme, theme);
|
||||
_currentTheme = theme;
|
||||
ThemeChanged?.Invoke(CurrentTheme, theme);
|
||||
CurrentTheme = theme;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the application's theme based on system settings and user preferences.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This considers:
|
||||
/// - Whether a High Contrast theme is active in Windows.
|
||||
/// - The system-wide app mode preference (Light or Dark).
|
||||
/// - The user's preference override for Light or Dark mode in the application settings.
|
||||
/// </remarks>
|
||||
public void UpdateTheme()
|
||||
{
|
||||
ManagedCommon.Theme newTheme = _settings.Theme;
|
||||
ManagedCommon.Theme theme = ThemeExtensions.GetHighContrastBaseType();
|
||||
if (theme != ManagedCommon.Theme.Light)
|
||||
{
|
||||
newTheme = theme;
|
||||
}
|
||||
else if (_settings.Theme == ManagedCommon.Theme.System)
|
||||
{
|
||||
newTheme = ThemeExtensions.GetCurrentTheme();
|
||||
}
|
||||
Theme newTheme = _themeHelper.DetermineTheme(_settings.Theme);
|
||||
|
||||
_mainWindow.Dispatcher.Invoke(() =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace PowerLauncher.Services;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for interacting with the Windows Registry or an equivalent key-value data
|
||||
/// store.
|
||||
/// </summary>
|
||||
public interface IRegistryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the value associated with the specified name, in the specified registry key.
|
||||
/// If the name is not found in the specified key, returns the specified default value, or
|
||||
/// <c>null</c> if the specified key does not exist.
|
||||
/// </summary>
|
||||
/// <param name="keyName">The full registry path of the key, beginning with a valid registry
|
||||
/// root, such as "HKEY_CURRENT_USER".</param>
|
||||
/// <param name="valueName">The name of the name/value pair.</param>
|
||||
/// <param name="defaultValue">The value to return if <see cref="valueName"/> does not exist.
|
||||
/// </param>
|
||||
/// <returns><c>null</c> if the subkey specified by <paramref name="keyName"/> does not exist;
|
||||
/// otherwise, the value associated with <paramref name="valueName"/>, or
|
||||
/// <paramref name="defaultValue"/> if <paramref name="valueName"/> is not found.</returns>
|
||||
/// <exception cref="ArgumentException"><paramref name="keyName"/> does not begin with a valid
|
||||
/// registry root.</exception>
|
||||
/// <exception cref="UnauthorizedAccessException">Thrown if access to the registry or
|
||||
/// equivalent store is denied.</exception>
|
||||
/// <remarks>Implementations may throw additional exceptions depending on their internal
|
||||
/// storage mechanism.</remarks>
|
||||
object? GetValue(string keyName, string? valueName, object? defaultValue);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the specified name/value pair on the specified registry key. If the specified key does
|
||||
/// not exist, it is created.
|
||||
/// </summary>
|
||||
/// <param name="keyName">The full registry path of the key, beginning with a valid registry
|
||||
/// root, such as "HKEY_CURRENT_USER".</param>
|
||||
/// <param name="valueName">The name of the name/value pair.</param>
|
||||
/// <param name="value">The value to be stored.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="keyName"/> does not begin with a valid registry root.
|
||||
///
|
||||
/// -or-
|
||||
///
|
||||
/// <paramref name="keyName"> is longer than the maximum length allowed (255 characters).
|
||||
/// </exception>
|
||||
/// <exception cref="UnauthorizedAccessException">Access to the key is denied; for example,
|
||||
/// it is a root-level node, or the key has not been opened with write access.</exception>
|
||||
void SetValue(string keyName, string? valueName, object value);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the specified name/value pair on the specified registry key. If the specified key does
|
||||
/// not exist, it is created.
|
||||
/// </summary>
|
||||
/// <param name="keyName">The full registry path of the key, beginning with a valid registry
|
||||
/// root, such as "HKEY_CURRENT_USER".</param>
|
||||
/// <param name="valueName">The name of the name/value pair.</param>
|
||||
/// <param name="value">The value to be stored.</param>
|
||||
/// <param name="valueKind">The registry data type to use when storing the data.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="keyName"/> does not begin with a valid registry root.
|
||||
///
|
||||
/// -or-
|
||||
///
|
||||
/// <paramref name="keyName"> is longer than the maximum length allowed (255 characters).
|
||||
///
|
||||
/// -or-
|
||||
///
|
||||
/// The type of <paramref name="value"/> did not match the registry data type specified by
|
||||
/// <paramref name="valueKind"/>, therefore the data could not be converted properly.
|
||||
/// </exception>
|
||||
/// <exception cref="UnauthorizedAccessException">Access to the key is denied; for example,
|
||||
/// it is a root-level node, or the key has not been opened with write access.</exception>
|
||||
void SetValue(string keyName, string? valueName, object value, RegistryValueKind valueKind);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace PowerLauncher.Services;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class RegistryService : IRegistryService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// <exception cref="SecurityException">The user does not have the permissions required to read
|
||||
/// from the registry key.</exception>
|
||||
/// <exception cref="IOException">The <see cref="RegistryKey"/> that contains the specified
|
||||
/// value has been marked for deletion.</exception>
|
||||
public object? GetValue(string keyName, string? valueName, object? defaultValue) =>
|
||||
Registry.GetValue(keyName, valueName, defaultValue);
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <exception cref="SecurityException">The user does not have the permissions required to
|
||||
/// create or modify registry keys.</exception>"
|
||||
public void SetValue(string keyName, string? valueName, object value) =>
|
||||
Registry.SetValue(keyName, valueName, value);
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <exception cref="SecurityException">The user does not have the permissions required to
|
||||
/// create or modify registry keys.</exception>
|
||||
public void SetValue(string keyName, string? valueName, object value, RegistryValueKind valueKind) =>
|
||||
Registry.SetValue(keyName, valueName, value, valueKind);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerLauncher.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating instances of <see cref="IRegistryService"/>.
|
||||
/// </summary>
|
||||
public static class RegistryServiceFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the default implementation of <see cref="IRegistryService"/>.
|
||||
/// </summary>
|
||||
/// <returns>An instance of the default <see cref="IRegistryService"/> implementation.</returns>
|
||||
public static IRegistryService Create() => new RegistryService();
|
||||
}
|
||||
124
src/modules/launcher/Wox.Test/ThemeHelperTest.cs
Normal file
124
src/modules/launcher/Wox.Test/ThemeHelperTest.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using ManagedCommon;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using PowerLauncher.Helper;
|
||||
using PowerLauncher.Services;
|
||||
|
||||
namespace Wox.Test;
|
||||
|
||||
[TestClass]
|
||||
public class ThemeHelperTest
|
||||
{
|
||||
// Registry key paths.
|
||||
private const string ThemesKey = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes";
|
||||
private const string PersonalizeKey = ThemesKey + "\\Personalize";
|
||||
|
||||
// Theme paths.
|
||||
private const string HighContrastThemePath = @"C:\WINDOWS\resources\Ease of Access Themes\hcwhite.theme";
|
||||
private const string NonHighContrastThemePath = @"C:\Users\Test\AppData\Local\Microsoft\Windows\Themes\Custom.theme";
|
||||
|
||||
/// <summary>
|
||||
/// The expected High Contrast theme when the <see cref="HighContrastThemePath"/> is returned
|
||||
/// from the registry.
|
||||
/// </summary>
|
||||
private const Theme HighContrastTheme = Theme.HighContrastWhite;
|
||||
|
||||
/// <summary>
|
||||
/// Mock <see cref="IRegistryService.GetValue"/>, to return the value of the AppsUseLightTheme
|
||||
/// key.
|
||||
/// </summary>
|
||||
private static readonly Expression<Func<IRegistryService, object>> _mockAppsUseLightTheme = (service) =>
|
||||
service.GetValue(PersonalizeKey, "AppsUseLightTheme", ThemeHelper.AppsUseLightThemeDefault);
|
||||
|
||||
/// <summary>
|
||||
/// Mock <see cref="IRegistryService.GetValue"/> to return the value of the CurrentTheme key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default value given here - string.Empty - must be the same as the default value in the
|
||||
/// actual code for tests using this mock to be valid.
|
||||
/// </remarks>
|
||||
private static readonly Expression<Func<IRegistryService, object>> _mockCurrentTheme = (service) =>
|
||||
service.GetValue(ThemesKey, "CurrentTheme", string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Test GetAppsTheme method.
|
||||
/// </summary>
|
||||
/// <param name="registryValue">The mocked value for the AppsUseLightTheme registry key.</param>
|
||||
/// <param name="expectedTheme">The expected <see cref="Theme"/> output from the call to
|
||||
/// <see cref="ThemeHelper.GetAppsTheme"/>.</param>
|
||||
[DataTestMethod]
|
||||
[DataRow(ThemeHelper.AppsUseLightThemeLight, Theme.Light)]
|
||||
[DataRow(ThemeHelper.AppsUseLightThemeDark, Theme.Dark)]
|
||||
[DataRow(int.MaxValue, Theme.Light)] // Out of range values should default to Light
|
||||
[DataRow(null, Theme.Light)] // Missing keys or values should default to Light
|
||||
[DataRow("RandomString", Theme.Light)] // Invalid string values should default to Light
|
||||
public void GetAppsTheme_ReturnsExpectedTheme(object registryValue, Theme expectedTheme)
|
||||
{
|
||||
var mockService = new Mock<IRegistryService>();
|
||||
mockService.Setup(_mockAppsUseLightTheme).Returns(registryValue);
|
||||
|
||||
var helper = new ThemeHelper(mockService.Object);
|
||||
|
||||
Assert.AreEqual(expectedTheme, helper.GetAppsTheme());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test <see cref="ThemeHelper.GetHighContrastTheme"/>.
|
||||
/// </summary>
|
||||
/// <param name="registryValue">The mocked value for the CurrentTheme registry key.</param>
|
||||
/// <param name="expectedTheme">The expected <see cref="Theme"/> output from the call to
|
||||
/// <see cref="ThemeHelper.GetHighContrastTheme"/>.</param>
|
||||
[DataTestMethod]
|
||||
[DataRow(HighContrastThemePath, HighContrastTheme)] // Valid High Contrast theme
|
||||
[DataRow(NonHighContrastThemePath, null)] // Non-High Contrast theme should return null
|
||||
[DataRow(null, null)] // Missing keys or values should default to null
|
||||
[DataRow("", null)] // Empty string values should default to null
|
||||
public void GetHighContrastTheme_ReturnsExpectedTheme(string registryValue, Theme? expectedTheme)
|
||||
{
|
||||
var mockService = new Mock<IRegistryService>();
|
||||
mockService.Setup(_mockCurrentTheme).Returns(registryValue);
|
||||
|
||||
var helper = new ThemeHelper(mockService.Object);
|
||||
|
||||
Assert.AreEqual(expectedTheme, helper.GetHighContrastTheme());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test <see cref="ThemeHelper.DetermineTheme"/>.
|
||||
/// </summary>
|
||||
/// <param name="registryTheme">The mocked value for the CurrentTheme registry key.</param>
|
||||
/// <param name="requestedTheme">The <see cref="Theme"/> value from the application's settings.
|
||||
/// </param>
|
||||
/// <param name="expectedTheme">The expected <see cref="Theme"/> output from the call to
|
||||
/// <see cref="ThemeHelper.DetermineTheme"/>.</param>
|
||||
/// <param name="appsUseLightTheme">The mocked value for the AppsUseLightTheme registry key,
|
||||
/// representing the system preference for Light or Dark mode.</param>
|
||||
[DataTestMethod]
|
||||
[DataRow(HighContrastThemePath, Theme.System, HighContrastTheme)] // High Contrast theme active
|
||||
[DataRow(HighContrastThemePath, Theme.Light, HighContrastTheme)] // High Contrast theme active - Light mode override ignored
|
||||
[DataRow(HighContrastThemePath, Theme.Dark, HighContrastTheme)] // High Contrast theme active - Dark mode override ignored
|
||||
[DataRow(NonHighContrastThemePath, Theme.System, Theme.Light)] // System preference with default light theme
|
||||
[DataRow(NonHighContrastThemePath, Theme.System, Theme.Dark, ThemeHelper.AppsUseLightThemeDark)] // System preference with dark mode
|
||||
[DataRow(NonHighContrastThemePath, Theme.Light, Theme.Light, ThemeHelper.AppsUseLightThemeDark)] // Light mode override
|
||||
[DataRow(NonHighContrastThemePath, Theme.Dark, Theme.Dark, ThemeHelper.AppsUseLightThemeLight)] // Dark mode override
|
||||
[DataRow(null, Theme.System, Theme.Light)] // Missing keys or values should default to Light
|
||||
[DataRow("", Theme.System, Theme.Light)] // Empty current theme paths should default to Light
|
||||
[DataRow("RandomString", Theme.System, Theme.Light)] // Invalid current theme paths should default to Light
|
||||
[DataRow(NonHighContrastThemePath, (Theme)int.MaxValue, Theme.Light)] // Invalid theme values should default to Light
|
||||
public void DetermineTheme_ReturnsExpectedTheme(string registryTheme, Theme requestedTheme, Theme expectedTheme, int? appsUseLightTheme = 1)
|
||||
{
|
||||
var mockService = new Mock<IRegistryService>();
|
||||
mockService.Setup(_mockCurrentTheme).Returns(registryTheme);
|
||||
mockService.Setup(_mockAppsUseLightTheme).Returns(appsUseLightTheme);
|
||||
|
||||
var helper = new ThemeHelper(mockService.Object);
|
||||
|
||||
Assert.AreEqual(expectedTheme, helper.DetermineTheme(requestedTheme));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user