Compare commits

..

5 Commits

8 changed files with 80 additions and 141 deletions

View File

@@ -7,7 +7,6 @@
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml" />
<ResourceDictionary Source="pack://application:,,,/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -153,6 +153,23 @@ namespace PowerLauncher
_stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision;
_mainVM = new MainViewModel(_settings, NativeThreadCTS.Token);
// Set ThemeMode before MainWindow creation so Fluent static resources
// (e.g. DefaultTextBoxStyle, CaptionTextBlockStyle referenced via BasedOn)
// are available for XAML parsing. ThemeMode.None cannot be used here even for
// high-contrast themes, because WPF's BasedOn StaticResource lookups require
// the named Fluent resources to exist at parse time.
// HighContrastWhite → Light (visually closer); other HC → Dark fallback.
// ThemeManager.SetSystemTheme will apply the correct ThemeMode (None for HC)
// once the window is ready.
var themeHelper = new ThemeHelper();
var initialTheme = themeHelper.DetermineTheme(_settings.Theme);
#pragma warning disable WPF0001
Application.Current.ThemeMode = initialTheme is Theme.Light or Theme.HighContrastWhite
? ThemeMode.Light
: ThemeMode.Dark;
#pragma warning restore WPF0001
_mainWindow = new MainWindow(_settings, _mainVM, NativeThreadCTS.Token);
_themeManager = new ThemeManager(_settings, _mainWindow);
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet, _themeManager);

View File

@@ -61,7 +61,7 @@ namespace PowerLauncher.Helper
// Many bug reports because users see the "Report problem UI" after "the" crash with System.Runtime.InteropServices.COMException 0xD0000701 or 0x80263001.
// However, displaying this "Report problem UI" during WPF crashes, especially when DWM composition is changing, is not ideal; some users reported it hangs for up to a minute before the "Report problem UI" appears.
// This change modifies the behavior to log the exception instead of showing the "Report problem UI".
if (ExceptionHelper.IsRecoverableDwmCompositionException(e))
if (ExceptionHelper.IsRecoverableDwmCompositionException(e as System.Runtime.InteropServices.COMException))
{
var logger = LogManager.GetLogger(LoggerName);
logger.Error($"From {(isNotUIThread ? "non" : string.Empty)} UI thread's exception: {ExceptionFormatter.FormatException(e)}");

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Reflection;
using System.Runtime.InteropServices;
namespace PowerLauncher.Helper
@@ -23,13 +22,6 @@ namespace PowerLauncher.Helper
/// </summary>
internal static bool IsRecoverableDwmCompositionException(Exception exception)
{
// Unwrap TargetInvocationException to get the underlying exception, since WPF's internal
// theme-change mechanism invokes handlers via reflection which wraps exceptions this way.
if (exception is TargetInvocationException && exception.InnerException != null)
{
exception = exception.InnerException;
}
if (exception is not COMException comException)
{
return false;

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@@ -22,6 +23,7 @@ namespace PowerLauncher.Helper
private readonly ThemeHelper _themeHelper = new();
private bool _disposed;
private bool _isHighContrastMode;
private CancellationTokenSource _themeUpdateTokenSource;
private const int MaxRetries = 5;
private const int InitialDelayMs = 2000;
@@ -30,6 +32,15 @@ namespace PowerLauncher.Helper
public event Common.UI.ThemeChangedHandler ThemeChanged;
internal static ThemeMode GetThemeMode(Theme theme) => theme switch
{
Theme.Light => ThemeMode.Light,
Theme.Dark => ThemeMode.Dark,
Theme.HighContrastBlack or Theme.HighContrastWhite or Theme.HighContrastOne or
Theme.HighContrastTwo => ThemeMode.None,
_ => ThemeMode.Dark,
};
public ThemeManager(PowerToysRunSettings settings, MainWindow mainWindow)
{
_settings = settings;
@@ -50,14 +61,22 @@ namespace PowerLauncher.Helper
{
_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 == Theme.Light ? ThemeMode.Light : ThemeMode.Dark;
#pragma warning restore WPF0001
if (theme is Theme.Dark or Theme.Light)
{
// When returning from a high-contrast theme, clear the high-contrast resource
// dictionaries that were applied to the window.
if (_isHighContrastMode)
{
_mainWindow.Resources.MergedDictionaries.Clear();
_isHighContrastMode = false;
}
// 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 = GetThemeMode(theme);
#pragma warning restore WPF0001
if (!OSVersionHelper.IsWindows11())
{
// Apply background only on Windows 10
@@ -70,6 +89,14 @@ namespace PowerLauncher.Helper
}
else
{
// For high-contrast themes, disable WPF's Fluent theme manager to avoid conflicts
// with the custom high-contrast resource dictionaries.
#pragma warning disable WPF0001
Application.Current.ThemeMode = GetThemeMode(theme);
#pragma warning restore WPF0001
_isHighContrastMode = true;
string styleThemeString = theme switch
{
Theme.HighContrastOne => "Themes/HighContrast1.xaml",
@@ -159,16 +186,12 @@ namespace PowerLauncher.Helper
return;
}
catch (Exception ex) when (ExceptionHelper.IsRecoverableDwmCompositionException(ex))
catch (COMException ex) when (ExceptionHelper.IsRecoverableDwmCompositionException(ex))
{
var logHResult = (ex is System.Reflection.TargetInvocationException tie && tie.InnerException != null)
? tie.InnerException.HResult
: ex.HResult;
switch (attempt)
{
case 1:
Log.Warn($"Desktop composition is disabled (HRESULT: 0x{logHResult:X}). Scheduling retries for theme update.", typeof(ThemeManager));
Log.Warn($"Desktop composition is disabled (HRESULT: 0x{ex.HResult:X}). Scheduling retries for theme update.", typeof(ThemeManager));
delayMs = InitialDelayMs;
break;
case < maxAttempts:

View File

@@ -50,7 +50,7 @@
</Style>
<Style x:Key="SubtleButtonStyle" TargetType="{x:Type Button}">
<Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}" />
<Setter Property="FocusVisualStyle" Value="{DynamicResource FocusVisual}" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Focusable" Value="False" />

View File

@@ -1,118 +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.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerLauncher.Helper;
namespace Wox.Test;
[TestClass]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Win32 naming conventions")]
public class ExceptionHelperTest
{
private const int DWM_E_COMPOSITIONDISABLED = unchecked((int)0x80263001);
private const int STATUS_MESSAGE_LOST_HR = unchecked((int)0xD0000701);
private const string PresentationFrameworkSource = "PresentationFramework";
[TestMethod]
public void IsRecoverableDwmCompositionException_NullException_ReturnsFalse()
{
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(null));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_NonCOMException_ReturnsFalse()
{
var ex = new InvalidOperationException("Test");
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_COMException_CompositionDisabled_ReturnsTrue()
{
var ex = new COMException("Desktop composition is disabled", DWM_E_COMPOSITIONDISABLED);
Assert.IsTrue(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_COMException_OtherHResult_ReturnsFalse()
{
var ex = new COMException("Some other COM error", unchecked((int)0x80004005));
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingCompositionDisabled_ReturnsTrue()
{
var inner = new COMException("Desktop composition is disabled", DWM_E_COMPOSITIONDISABLED);
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsTrue(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingMessageLostFromPresentationFramework_ReturnsTrue()
{
var inner = new COMException("Message lost", STATUS_MESSAGE_LOST_HR)
{
Source = PresentationFrameworkSource,
};
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsTrue(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingDwmCompositionChangedStackTrace_ReturnsTrue()
{
var inner = CreateDwmCompositionChangedComException();
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsTrue(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingUnrelatedCOMException_ReturnsFalse()
{
var inner = new COMException("Unrelated", unchecked((int)0x80004005));
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingNonCOMException_ReturnsFalse()
{
var inner = new InvalidOperationException("Not a COM exception");
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_NullInner_ReturnsFalse()
{
var ex = new TargetInvocationException("No inner", null);
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
private static COMException CreateDwmCompositionChangedComException()
{
try
{
ThrowDwmCompositionChangedComException();
throw new AssertFailedException("Expected COMException to be thrown.");
}
catch (COMException ex)
{
return ex;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowDwmCompositionChangedComException()
{
throw new COMException("DWM composition changed", unchecked((int)0x80004005));
}
}

View File

@@ -0,0 +1,26 @@
// 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.Windows;
using ManagedCommon;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerLauncher.Helper;
namespace Wox.Test;
[TestClass]
public class ThemeManagerTest
{
[DataTestMethod]
[DataRow(Theme.Light, ThemeMode.Light)]
[DataRow(Theme.Dark, ThemeMode.Dark)]
[DataRow(Theme.HighContrastBlack, ThemeMode.None)]
[DataRow(Theme.HighContrastWhite, ThemeMode.None)]
[DataRow(Theme.HighContrastOne, ThemeMode.None)]
[DataRow(Theme.HighContrastTwo, ThemeMode.None)]
public void GetThemeMode_ReturnsExpectedThemeMode(Theme theme, ThemeMode expectedThemeMode)
{
Assert.AreEqual(expectedThemeMode, ThemeManager.GetThemeMode(theme));
}
}