diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index c3bb6190f01..77e1ea5b5bd 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -11,12 +11,12 @@ using System.Windows.Media; using System.Windows.Media.Effects; using System.Windows.Shell; -using System.Windows.Threading; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; using Microsoft.Win32; +using System.Windows.Threading; namespace Flow.Launcher.Core.Resource { @@ -100,12 +100,6 @@ private void MakeSureThemeDirectoriesExist() private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) { - // Add new resources - if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate)) - { - Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate); - } - // Remove old resources if (_oldResource != null && _oldResource != dictionaryToUpdate && Application.Current.Resources.MergedDictionaries.Contains(_oldResource)) @@ -113,7 +107,20 @@ private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) Application.Current.Resources.MergedDictionaries.Remove(_oldResource); } - _oldResource = dictionaryToUpdate; + // Add new resources + try + { + if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate)) + { + Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate); + } + _oldResource = dictionaryToUpdate; + } + catch (InvalidCastException) + { + // System.InvalidCastException: Unable to cast object of type 'System.Windows.Media.Color' to type 'System.Windows.Expression'. + _oldResource = null; + } } /// @@ -124,8 +131,8 @@ public void UpdateFonts() try { // Load a ResourceDictionary for the specified theme. - var themeName = _settings.Theme; - var dict = GetThemeResourceDictionary(themeName); + var theme = _settings.Theme; + var dict = GetThemeResourceDictionary(theme); // Apply font settings to the theme resource. ApplyFontSettings(dict); @@ -221,7 +228,13 @@ private static void SetFontProperties(Style style, FontFamily fontFamily, FontSt var foregroundPropertyValue = style.Setters.OfType().Where(x => x.Property.Name == "Foreground") .Select(x => x.Value).FirstOrDefault(); if (!caretBrushPropertyValue && foregroundPropertyValue != null) - style.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue)); + { + var newCaretValue = GetNewCaretValue(foregroundPropertyValue); + if (newCaretValue != null) + { + style.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, newCaretValue)); + } + } } else { @@ -246,6 +259,37 @@ private static void SetFontProperties(Style style, FontFamily fontFamily, FontSt } } + private static object GetNewCaretValue(object foregroundPropertyValue) + { + object newCaretValue; + if (foregroundPropertyValue is DynamicResourceExtension dynamicResource) + { + newCaretValue = new DynamicResourceExtension(dynamicResource.ResourceKey); + } + else if (foregroundPropertyValue is SolidColorBrush solidBrush) + { + // Create a new brush to avoid sharing mutable freezables with potential expressions + if (solidBrush.IsFrozen) + { + newCaretValue = solidBrush; + } + else + { + var newBrush = new SolidColorBrush(solidBrush.Color) + { + Opacity = solidBrush.Opacity + }; + if (newBrush.CanFreeze) newBrush.Freeze(); + newCaretValue = newBrush; + } + } + else + { + newCaretValue = foregroundPropertyValue; + } + return newCaretValue; + } + private ResourceDictionary GetThemeResourceDictionary(string theme) { var uri = GetThemePath(theme); @@ -275,10 +319,15 @@ private ResourceDictionary GetResourceDictionary(string theme) queryBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); var caretBrushPropertyValue = queryBoxStyle.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); - var foregroundPropertyValue = queryBoxStyle.Setters.OfType().Where(x => x.Property.Name == "Foreground") - .Select(x => x.Value).FirstOrDefault(); + var foregroundPropertyValue = queryBoxStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Foreground")?.Value; if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling - queryBoxStyle.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue)); + { + var newCaretValue = GetNewCaretValue(foregroundPropertyValue); + if (newCaretValue != null) + { + queryBoxStyle.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, newCaretValue)); + } + } // Query suggestion box's font style is aligned with query box querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); @@ -419,20 +468,17 @@ public bool ChangeTheme(string theme = null) if (string.IsNullOrEmpty(path)) throw new DirectoryNotFoundException($"Theme path can't be found <{path}>"); - // Retrieve theme resource – always use the resource with font settings applied. - var resourceDict = GetResourceDictionary(theme); - - UpdateResourceDictionary(resourceDict); - _settings.Theme = theme; - //always allow re-loading default theme, in case of failure of switching to a new theme from default theme + // Always allow re-loading default theme, in case of failure of switching to a new theme from default theme if (_oldTheme != theme || theme == Constant.DefaultTheme) { _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); } - BlurEnabled = IsBlurTheme(); + // Check if blur is enabled + var dict = GetThemeResourceDictionary(theme); + BlurEnabled = Win32Helper.IsBackdropSupported() && IsThemeBlurEnabled(dict); // Apply blur and drop shadow effect so that we do not need to call it again _ = RefreshFrameAsync(); @@ -477,76 +523,113 @@ public bool ChangeTheme(string theme = null) public void AddDropShadowEffectToCurrentTheme() { - var dict = GetCurrentResourceDictionary(); + // Get current theme's WindowBorderStyle + var theme = _settings.Theme; + var dict = GetThemeResourceDictionary(theme); + var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null; + if (windowBorderStyle == null) return; - var windowBorderStyle = dict["WindowBorderStyle"] as Style; + // Get a new unsealed style based on the old one, and copy Resources and Triggers + var newWindowBorderStyle = new Style(typeof(Border)); + CopyStyle(windowBorderStyle, newWindowBorderStyle); - var effectSetter = new Setter + // Identify existing Margin to calculate new Margin, and copy other setters + Setter existingMarginSetter = null; + foreach (var setterBase in windowBorderStyle.Setters) { - Property = UIElement.EffectProperty, - Value = new DropShadowEffect + if (setterBase is Setter setter) { - Opacity = 0.3, - ShadowDepth = 12, - Direction = 270, - BlurRadius = 30 + // Skip existing Margin (we'll replace it) + if (setter.Property == FrameworkElement.MarginProperty) + { + existingMarginSetter = setter; + continue; + } + + // Skip existing Effect (we'll ensure strictly one is added) + if (setter.Property == UIElement.EffectProperty) continue; } - }; - if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is not Setter marginSetter) - { - var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin); - marginSetter = new Setter() - { - Property = FrameworkElement.MarginProperty, - Value = margin, - }; - windowBorderStyle.Setters.Add(marginSetter); + // Add other setters (e.g. Background, BorderThickness) + newWindowBorderStyle.Setters.Add(setterBase); + } - SetResizeBoarderThickness(margin); + // Calculate new Margin + Thickness newMargin; + if (existingMarginSetter == null) + { + newMargin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin); } else { - var baseMargin = (Thickness)marginSetter.Value; - var newMargin = new Thickness( + var baseMargin = (Thickness)existingMarginSetter.Value; + newMargin = new Thickness( baseMargin.Left + ShadowExtraMargin, baseMargin.Top + ShadowExtraMargin, baseMargin.Right + ShadowExtraMargin, baseMargin.Bottom + ShadowExtraMargin); - marginSetter.Value = newMargin; - - SetResizeBoarderThickness(newMargin); } - windowBorderStyle.Setters.Add(effectSetter); + // Add new Margin Setter + newWindowBorderStyle.Setters.Add(new Setter(FrameworkElement.MarginProperty, newMargin)); - UpdateResourceDictionary(dict); + // Add Drop Shadow Effect Setter + newWindowBorderStyle.Setters.Add(new Setter + { + Property = UIElement.EffectProperty, + Value = new DropShadowEffect + { + Opacity = 0.3, + ShadowDepth = 12, + Direction = 270, + BlurRadius = 30 + } + }); + + SetResizeBoarderThickness(newMargin); + + Application.Current.Resources["WindowBorderStyle"] = newWindowBorderStyle; } public void RemoveDropShadowEffectFromCurrentTheme() { - var dict = GetCurrentResourceDictionary(); - var windowBorderStyle = dict["WindowBorderStyle"] as Style; + // Get current theme's WindowBorderStyle + var theme = _settings.Theme; + var dict = GetThemeResourceDictionary(theme); + var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null; + if (windowBorderStyle == null) return; - if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == UIElement.EffectProperty) is Setter effectSetter) - { - windowBorderStyle.Setters.Remove(effectSetter); - } + // Get a new unsealed style based on the old one, and copy Resources and Triggers + var newWindowBorderStyle = new Style(typeof(Border)); + CopyStyle(windowBorderStyle, newWindowBorderStyle); - if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is Setter marginSetter) + // Copy Setters, excluding the Effect setter and updating the Margin setter + foreach (var setterBase in windowBorderStyle.Setters) { - var currentMargin = (Thickness)marginSetter.Value; - var newMargin = new Thickness( - currentMargin.Left - ShadowExtraMargin, - currentMargin.Top - ShadowExtraMargin, - currentMargin.Right - ShadowExtraMargin, - currentMargin.Bottom - ShadowExtraMargin); - marginSetter.Value = newMargin; + if (setterBase is Setter setter) + { + // Skip existing Effect (We'll remove it) + if (setter.Property == UIElement.EffectProperty) continue; + + // Update Margin by subtracting the extra margin we added for the shadow + if (setter.Property == FrameworkElement.MarginProperty) + { + var currentMargin = (Thickness)setter.Value; + var newMargin = new Thickness( + currentMargin.Left - ShadowExtraMargin, + currentMargin.Top - ShadowExtraMargin, + currentMargin.Right - ShadowExtraMargin, + currentMargin.Bottom - ShadowExtraMargin); + newWindowBorderStyle.Setters.Add(new Setter(FrameworkElement.MarginProperty, newMargin)); + continue; + } + } + newWindowBorderStyle.Setters.Add(setterBase); } SetResizeBoarderThickness(null); - UpdateResourceDictionary(dict); + Application.Current.Resources["WindowBorderStyle"] = newWindowBorderStyle; } public void SetResizeBorderThickness(WindowChrome windowChrome, bool fixedWindowSize) @@ -596,26 +679,33 @@ private void SetResizeBoarderThickness(Thickness? effectMargin) /// public async Task RefreshFrameAsync() { - await Application.Current.Dispatcher.InvokeAsync(() => + if (Application.Current == null) return; + if (!Application.Current.Dispatcher.CheckAccess()) { - // Get the actual backdrop type and drop shadow effect settings - var (backdropType, useDropShadowEffect) = GetActualValue(); + await Application.Current?.Dispatcher.InvokeAsync(RefreshFrameAsync, DispatcherPriority.Render); + return; + } - // Remove OS minimizing/maximizing animation - // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3); + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, useDropShadowEffect) = GetActualValue(); - // The timing of adding the shadow effect should vary depending on whether the theme is transparent. - if (BlurEnabled) - { - AutoDropShadow(useDropShadowEffect); - } - SetBlurForWindow(_settings.Theme, backdropType); + // Remove OS minimizing/maximizing animation + // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3); - if (!BlurEnabled) - { - AutoDropShadow(useDropShadowEffect); - } - }, DispatcherPriority.Render); + // The timing of adding the shadow effect should vary depending on whether the theme is transparent. + if (BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + +#pragma warning disable VSTHRD103 // Call async methods when in an async method + SetBlurForWindow(_settings.Theme, backdropType); +#pragma warning restore VSTHRD103 // Call async methods when in an async method + + if (!BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } } /// @@ -623,13 +713,17 @@ await Application.Current.Dispatcher.InvokeAsync(() => /// public async Task SetBlurForWindowAsync() { - await Application.Current.Dispatcher.InvokeAsync(() => + if (Application.Current == null) return; + if (!Application.Current.Dispatcher.CheckAccess()) { - // Get the actual backdrop type and drop shadow effect settings - var (backdropType, _) = GetActualValue(); + await Application.Current?.Dispatcher.InvokeAsync(SetBlurForWindowAsync, DispatcherPriority.Render); + return; + } - SetBlurForWindow(_settings.Theme, backdropType); - }, DispatcherPriority.Render); + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, _) = GetActualValue(); + + SetBlurForWindow(_settings.Theme, backdropType); } /// @@ -667,21 +761,25 @@ private void SetBlurForWindow(string theme, BackdropTypes backdropType) if (mainWindow == null) return; // Check if the theme supports blur - bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b; + var hasBlur = IsThemeBlurEnabled(dict); if (BlurEnabled && hasBlur && Win32Helper.IsBackdropSupported()) { // If the BackdropType is Mica or MicaAlt, set the windowborderstyle's background to transparent if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt) { windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Background")); - windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)))); + var brush = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); + brush.Freeze(); + windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, brush)); } else if (backdropType == BackdropTypes.Acrylic) { windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Background")); - windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Colors.Transparent))); + var brush = new SolidColorBrush(Colors.Transparent); + brush.Freeze(); + windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, brush)); } - + // For themes with blur enabled, the window border is rendered by the system, so it's treated as a simple rectangle regardless of thickness. //(This is to avoid issues when the window is forcibly changed to a rectangular shape during snap scenarios.) var cornerRadiusSetter = windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property == Border.CornerRadiusProperty); @@ -689,7 +787,7 @@ private void SetBlurForWindow(string theme, BackdropTypes backdropType) cornerRadiusSetter.Value = new CornerRadius(0); else windowBorderStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(0))); - + // Apply the blur effect Win32Helper.DWMSetBackdropForWindow(mainWindow, backdropType); ColorizeWindow(theme, backdropType); @@ -706,13 +804,12 @@ private void SetBlurForWindow(string theme, BackdropTypes backdropType) private void AutoDropShadow(bool useDropShadowEffect) { - SetWindowCornerPreference("Default"); - RemoveDropShadowEffectFromCurrentTheme(); if (useDropShadowEffect) { if (BlurEnabled && Win32Helper.IsBackdropSupported()) { SetWindowCornerPreference("Round"); + RemoveDropShadowEffectFromCurrentTheme(); } else { @@ -725,9 +822,11 @@ private void AutoDropShadow(bool useDropShadowEffect) if (BlurEnabled && Win32Helper.IsBackdropSupported()) { SetWindowCornerPreference("Default"); + RemoveDropShadowEffectFromCurrentTheme(); } else { + SetWindowCornerPreference("Default"); RemoveDropShadowEffectFromCurrentTheme(); } } @@ -746,8 +845,9 @@ private static void SetWindowCornerPreference(string cornerType) // for theme has not "LightBG" or "DarkBG" case. private Color GetWindowBorderStyleBackground(string theme) { - var Resources = GetThemeResourceDictionary(theme); - var windowBorderStyle = (Style)Resources["WindowBorderStyle"]; + var dict = GetThemeResourceDictionary(theme); + var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null; + if (windowBorderStyle == null) return Colors.Transparent; // Default is transparent var backgroundSetter = windowBorderStyle.Setters .OfType() @@ -765,12 +865,12 @@ private Color GetWindowBorderStyleBackground(string theme) else if (backgroundValue is DynamicResourceExtension dynamicResource) { // When DynamicResource Extension it is, Key is resource's name. - var resourceKey = backgroundSetter.Value.ToString(); + var resourceKey = dynamicResource.ResourceKey.ToString(); // find key in resource and return color. - if (Resources.Contains(resourceKey)) + if (dict.Contains(resourceKey)) { - var colorResource = Resources[resourceKey]; + var colorResource = dict[resourceKey]; if (colorResource is SolidColorBrush colorBrush) { return colorBrush.Color; @@ -803,7 +903,9 @@ private void ApplyPreviewBackground(Color? bgColor = null) // Apply background color (remove transparency in color) Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B); - previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor))); + var brush = new SolidColorBrush(backgroundColor); + brush.Freeze(); + previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, brush)); // The blur theme keeps the corner round fixed (applying DWM code to modify it causes rendering issues). // The non-blur theme retains the previously set WindowBorderStyle. @@ -817,7 +919,7 @@ private void ApplyPreviewBackground(Color? bgColor = null) Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle; } - private void CopyStyle(Style originalStyle, Style targetStyle) + private static void CopyStyle(Style originalStyle, Style targetStyle) { // If the style is based on another style, copy the base style first if (originalStyle.BasedOn != null) @@ -841,20 +943,16 @@ private void ColorizeWindow(string theme, BackdropTypes backdropType) if (mainWindow == null) return; // Check if the theme supports blur - bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b; + var hasBlur = IsThemeBlurEnabled(dict); // SystemBG value check (Auto, Light, Dark) - string systemBG = dict.Contains("SystemBG") ? dict["SystemBG"] as string : "Auto"; // 기본값 Auto + var systemBG = dict.Contains("SystemBG") ? dict["SystemBG"] as string : "Auto"; // 기본값 Auto // Check the user's ColorScheme setting - string colorScheme = _settings.ColorScheme; - - // Check system dark mode setting (read AppsUseLightTheme value) - int themeValue = (int)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 1); - bool isSystemDark = themeValue == 0; + var colorScheme = _settings.ColorScheme; // Final decision on whether to use dark mode - bool useDarkMode = false; + var useDarkMode = false; // If systemBG is not "Auto", prioritize it over ColorScheme and set the mode based on systemBG value if (systemBG == "Dark") @@ -869,11 +967,20 @@ private void ColorizeWindow(string theme, BackdropTypes backdropType) { // If systemBG is "Auto", decide based on ColorScheme if (colorScheme == "Dark") + { useDarkMode = true; + } else if (colorScheme == "Light") + { useDarkMode = false; + } else + { + // Check system dark mode setting (read AppsUseLightTheme value) + var themeValue = (int)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 1); + var isSystemDark = themeValue == 0; useDarkMode = isSystemDark; // Auto (based on system setting) + } } // Apply DWM Dark Mode @@ -906,8 +1013,8 @@ private void ColorizeWindow(string theme, BackdropTypes backdropType) Color selectedBG = useDarkMode ? DarkBG : LightBG; ApplyPreviewBackground(selectedBG); - bool isBlurAvailable = hasBlur && Win32Helper.IsBackdropSupported(); // Windows 11 미만이면 hasBlur를 강제 false - + // If Windows does not support backdrop, treat as blur unavailable + var isBlurAvailable = hasBlur && Win32Helper.IsBackdropSupported(); if (!isBlurAvailable) { mainWindow.Background = Brushes.Transparent; @@ -917,23 +1024,22 @@ private void ColorizeWindow(string theme, BackdropTypes backdropType) // Only set the background to transparent if the theme supports blur if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt) { - mainWindow.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); + var backgroundBrush = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); + backgroundBrush.Freeze(); + mainWindow.Background = backgroundBrush; } else { - mainWindow.Background = new SolidColorBrush(selectedBG); + var backgroundBrush = new SolidColorBrush(selectedBG); + backgroundBrush.Freeze(); + mainWindow.Background = backgroundBrush; } } } - private static bool IsBlurTheme() + private static bool IsThemeBlurEnabled(ResourceDictionary dict) { - if (!Win32Helper.IsBackdropSupported()) // Windows 11 미만이면 무조건 false - return false; - - var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); - - return resource is bool b && b; + return dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool enabled && enabled; } #endregion