/ src / modules / launcher / PowerLauncher / Helper / ThemeManager.cs
ThemeManager.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.Runtime.InteropServices;
  7  using System.Threading;
  8  using System.Threading.Tasks;
  9  using System.Windows;
 10  using System.Windows.Media;
 11  using ManagedCommon;
 12  using Microsoft.Win32;
 13  using Wox.Infrastructure.Image;
 14  using Wox.Infrastructure.UserSettings;
 15  using Wox.Plugin.Logger;
 16  
 17  namespace PowerLauncher.Helper
 18  {
 19      public class ThemeManager : IDisposable
 20      {
 21          private readonly PowerToysRunSettings _settings;
 22          private readonly MainWindow _mainWindow;
 23          private readonly ThemeHelper _themeHelper = new();
 24  
 25          private bool _disposed;
 26          private CancellationTokenSource _themeUpdateTokenSource;
 27          private const int MaxRetries = 5;
 28          private const int InitialDelayMs = 2000;
 29  
 30          public Theme CurrentTheme { get; private set; }
 31  
 32          public event Common.UI.ThemeChangedHandler ThemeChanged;
 33  
 34          public ThemeManager(PowerToysRunSettings settings, MainWindow mainWindow)
 35          {
 36              _settings = settings;
 37              _mainWindow = mainWindow;
 38              UpdateTheme();
 39              SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
 40          }
 41  
 42          private void OnUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
 43          {
 44              if (e.Category == UserPreferenceCategory.General)
 45              {
 46                  UpdateTheme();
 47              }
 48          }
 49  
 50          private void SetSystemTheme(Theme theme)
 51          {
 52              _mainWindow.Background = !OSVersionHelper.IsWindows11() ? SystemColors.WindowBrush : null;
 53  
 54              // Need to disable WPF0001 since setting Application.Current.ThemeMode is experimental
 55              // https://learn.microsoft.com/en-us/dotnet/desktop/wpf/whats-new/net90#set-in-code
 56  #pragma warning disable WPF0001
 57              Application.Current.ThemeMode = theme == Theme.Light ? ThemeMode.Light : ThemeMode.Dark;
 58  #pragma warning restore WPF0001
 59  
 60              if (theme is Theme.Dark or Theme.Light)
 61              {
 62                  if (!OSVersionHelper.IsWindows11())
 63                  {
 64                      // Apply background only on Windows 10
 65                      // Windows theme does not work properly for dark and light mode so right now set the background color manually.
 66                      _mainWindow.Background = new SolidColorBrush
 67                      {
 68                          Color = (Color)ColorConverter.ConvertFromString(theme == Theme.Dark ? "#202020" : "#fafafa"),
 69                      };
 70                  }
 71              }
 72              else
 73              {
 74                  string styleThemeString = theme switch
 75                  {
 76                      Theme.HighContrastOne => "Themes/HighContrast1.xaml",
 77                      Theme.HighContrastTwo => "Themes/HighContrast2.xaml",
 78                      Theme.HighContrastWhite => "Themes/HighContrastWhite.xaml",
 79                      Theme.HighContrastBlack => "Themes/HighContrastBlack.xaml",
 80                      _ => "Themes/Light.xaml",
 81                  };
 82  
 83                  _mainWindow.Resources.MergedDictionaries.Clear();
 84                  _mainWindow.Resources.MergedDictionaries.Add(new ResourceDictionary
 85                  {
 86                      Source = new Uri(styleThemeString, UriKind.Relative),
 87                  });
 88  
 89                  if (OSVersionHelper.IsWindows11())
 90                  {
 91                      // Apply background only on Windows 11 to keep the same style as WPFUI
 92                      _mainWindow.Background = new SolidColorBrush
 93                      {
 94                          Color = (Color)_mainWindow.FindResource("LauncherBackgroundColor"),
 95                      };
 96                  }
 97              }
 98  
 99              ImageLoader.UpdateIconPath(theme);
100              ThemeChanged?.Invoke(CurrentTheme, theme);
101              CurrentTheme = theme;
102          }
103  
104          /// <summary>
105          /// Updates the application's theme based on system settings and user preferences.
106          /// </summary>
107          /// <remarks>
108          /// This considers:
109          /// - Whether a High Contrast theme is active in Windows.
110          /// - The system-wide app mode preference (Light or Dark).
111          /// - The user's preference override for Light or Dark mode in the application settings.
112          /// </remarks>
113          public void UpdateTheme()
114          {
115              Theme newTheme = _themeHelper.DetermineTheme(_settings.Theme);
116  
117              // Cancel any existing theme update operation
118              _themeUpdateTokenSource?.Cancel();
119              _themeUpdateTokenSource?.Dispose();
120              _themeUpdateTokenSource = new CancellationTokenSource();
121  
122              // Start theme update with retry logic in the background
123              _ = UpdateThemeWithRetryAsync(newTheme, _themeUpdateTokenSource.Token);
124          }
125  
126          /// <summary>
127          /// Applies the theme with retry logic for desktop composition errors.
128          /// </summary>
129          /// <param name="theme">The theme to apply.</param>
130          /// <param name="cancellationToken">Token to cancel the operation.</param>
131          private async Task UpdateThemeWithRetryAsync(Theme theme, CancellationToken cancellationToken)
132          {
133              var delayMs = 0;
134              const int maxAttempts = MaxRetries + 1;
135  
136              for (var attempt = 1; attempt <= maxAttempts; attempt++)
137              {
138                  try
139                  {
140                      if (delayMs > 0)
141                      {
142                          await Task.Delay(delayMs, cancellationToken);
143                      }
144  
145                      if (cancellationToken.IsCancellationRequested)
146                      {
147                          Log.Debug("Theme update operation was cancelled.", typeof(ThemeManager));
148                          return;
149                      }
150  
151                      await _mainWindow.Dispatcher.InvokeAsync(() =>
152                      {
153                          SetSystemTheme(theme);
154                      });
155  
156                      if (attempt > 1)
157                      {
158                          Log.Info($"Successfully applied theme after {attempt - 1} retry attempt(s).", typeof(ThemeManager));
159                      }
160  
161                      return;
162                  }
163                  catch (COMException ex) when (ExceptionHelper.IsRecoverableDwmCompositionException(ex))
164                  {
165                      switch (attempt)
166                      {
167                          case 1:
168                              Log.Warn($"Desktop composition is disabled (HRESULT: 0x{ex.HResult:X}). Scheduling retries for theme update.", typeof(ThemeManager));
169                              delayMs = InitialDelayMs;
170                              break;
171                          case < maxAttempts:
172                              Log.Warn($"Retry {attempt - 1}/{MaxRetries} failed: Desktop composition still disabled. Retrying in {delayMs * 2}ms...", typeof(ThemeManager));
173                              delayMs *= 2;
174                              break;
175                          default:
176                              Log.Exception($"Failed to set theme after {MaxRetries} retry attempts. Desktop composition remains disabled.", ex, typeof(ThemeManager));
177                              break;
178                      }
179                  }
180                  catch (OperationCanceledException)
181                  {
182                      Log.Debug("Theme update operation was cancelled.", typeof(ThemeManager));
183                      return;
184                  }
185                  catch (Exception ex)
186                  {
187                      Log.Exception($"Unexpected error during theme update (attempt {attempt}/{maxAttempts}): {ex.Message}", ex, typeof(ThemeManager));
188                      throw;
189                  }
190              }
191          }
192  
193          public void Dispose()
194          {
195              Dispose(true);
196              GC.SuppressFinalize(this);
197          }
198  
199          protected virtual void Dispose(bool disposing)
200          {
201              if (_disposed)
202              {
203                  return;
204              }
205  
206              if (disposing)
207              {
208                  SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
209                  _themeUpdateTokenSource?.Cancel();
210                  _themeUpdateTokenSource?.Dispose();
211              }
212  
213              _disposed = true;
214          }
215      }
216  }