diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs
index 76d83cbcf..3f07eef01 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -398,7 +398,16 @@ namespace Flow.Launcher.Core.Plugin
NonGlobalPlugins[newActionKeyword] = plugin;
}
+ // Update action keywords and action keyword in plugin metadata
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
+ if (plugin.Metadata.ActionKeywords.Count > 0)
+ {
+ plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
+ }
+ else
+ {
+ plugin.Metadata.ActionKeyword = string.Empty;
+ }
}
///
@@ -419,16 +428,15 @@ namespace Flow.Launcher.Core.Plugin
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
NonGlobalPlugins.Remove(oldActionkeyword);
-
+ // Update action keywords and action keyword in plugin metadata
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
- }
-
- public static void ReplaceActionKeyword(string id, string oldActionKeyword, string newActionKeyword)
- {
- if (oldActionKeyword != newActionKeyword)
+ if (plugin.Metadata.ActionKeywords.Count > 0)
{
- AddActionKeyword(id, newActionKeyword);
- RemoveActionKeyword(id, oldActionKeyword);
+ plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
+ }
+ else
+ {
+ plugin.Metadata.ActionKeyword = string.Empty;
}
}
diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs
index cda125a39..fc5c2e4e9 100644
--- a/Flow.Launcher.Core/Resource/Theme.cs
+++ b/Flow.Launcher.Core/Resource/Theme.cs
@@ -3,21 +3,29 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
+using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
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.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
+using Microsoft.Win32;
+using TextBox = System.Windows.Controls.TextBox;
namespace Flow.Launcher.Core.Resource
{
public class Theme
{
+ #region Properties & Fields
+
+ public bool BlurEnabled { get; set; }
+
private const string ThemeMetadataNamePrefix = "Name:";
private const string ThemeMetadataIsDarkPrefix = "IsDark:";
private const string ThemeMetadataHasBlurPrefix = "HasBlur:";
@@ -31,14 +39,12 @@ namespace Flow.Launcher.Core.Resource
private string _oldTheme;
private const string Folder = Constant.Themes;
private const string Extension = ".xaml";
- private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
- private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
+ private static string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
+ private static string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
- public string CurrentTheme => _settings.Theme;
+ #endregion
- public bool BlurEnabled { get; set; }
-
- private double mainWindowWidth;
+ #region Constructor
public Theme(IPublicAPI publicAPI, Settings settings)
{
@@ -66,6 +72,15 @@ namespace Flow.Launcher.Core.Resource
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
+ #endregion
+
+ #region Theme Resources
+
+ public string GetCurrentTheme()
+ {
+ return _settings.Theme;
+ }
+
private void MakeSureThemeDirectoriesExist()
{
foreach (var dir in _themeDirectories.Where(dir => !Directory.Exists(dir)))
@@ -81,59 +96,6 @@ namespace Flow.Launcher.Core.Resource
}
}
- public bool ChangeTheme(string theme)
- {
- const string defaultTheme = Constant.DefaultTheme;
-
- string path = GetThemePath(theme);
- try
- {
- if (string.IsNullOrEmpty(path))
- throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
-
- // reload all resources even if the theme itself hasn't changed in order to pickup changes
- // to things like fonts
- UpdateResourceDictionary(GetResourceDictionary(theme));
-
- _settings.Theme = theme;
-
-
- //always allow re-loading default theme, in case of failure of switching to a new theme from default theme
- if (_oldTheme != theme || theme == defaultTheme)
- {
- _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
- }
-
- BlurEnabled = Win32Helper.IsBlurTheme();
-
- if (_settings.UseDropShadowEffect && !BlurEnabled)
- AddDropShadowEffectToCurrentTheme();
-
- Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
- }
- catch (DirectoryNotFoundException)
- {
- Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
- if (theme != defaultTheme)
- {
- _api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
- ChangeTheme(defaultTheme);
- }
- return false;
- }
- catch (XamlParseException)
- {
- Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
- if (theme != defaultTheme)
- {
- _api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
- ChangeTheme(defaultTheme);
- }
- return false;
- }
- return true;
- }
-
private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate)
{
var dicts = Application.Current.Resources.MergedDictionaries;
@@ -154,9 +116,7 @@ namespace Flow.Launcher.Core.Resource
return dict;
}
- private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(_settings.Theme);
-
- public ResourceDictionary GetResourceDictionary(string theme)
+ private ResourceDictionary GetResourceDictionary(string theme)
{
var dict = GetThemeResourceDictionary(theme);
@@ -213,7 +173,7 @@ namespace Flow.Launcher.Core.Resource
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
Array.ForEach(
- new[] { resultSubItemStyle,resultSubItemSelectedStyle}, o
+ new[] { resultSubItemStyle, resultSubItemSelectedStyle }, o
=> Array.ForEach(setters, p => o.Setters.Add(p)));
}
@@ -221,28 +181,12 @@ namespace Flow.Launcher.Core.Resource
var windowStyle = dict["WindowStyle"] as Style;
var width = _settings.WindowSize;
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width));
- mainWindowWidth = (double)width;
return dict;
}
- private ResourceDictionary GetCurrentResourceDictionary( )
+ private ResourceDictionary GetCurrentResourceDictionary()
{
- return GetResourceDictionary(_settings.Theme);
- }
-
- public List LoadAvailableThemes()
- {
- List themes = new List();
- foreach (var themeDirectory in _themeDirectories)
- {
- var filePaths = Directory
- .GetFiles(themeDirectory)
- .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
- .Select(GetThemeDataFromPath);
- themes.AddRange(filePaths);
- }
-
- return themes.OrderBy(o => o.Name).ToList();
+ return GetResourceDictionary(GetCurrentTheme());
}
private ThemeData GetThemeDataFromPath(string path)
@@ -264,15 +208,15 @@ namespace Flow.Launcher.Core.Resource
{
if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase))
{
- name = line.Remove(0, ThemeMetadataNamePrefix.Length).Trim();
+ name = line[ThemeMetadataNamePrefix.Length..].Trim();
}
else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase))
{
- isDark = bool.Parse(line.Remove(0, ThemeMetadataIsDarkPrefix.Length).Trim());
+ isDark = bool.Parse(line[ThemeMetadataIsDarkPrefix.Length..].Trim());
}
else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase))
{
- hasBlur = bool.Parse(line.Remove(0, ThemeMetadataHasBlurPrefix.Length).Trim());
+ hasBlur = bool.Parse(line[ThemeMetadataHasBlurPrefix.Length..].Trim());
}
}
@@ -293,6 +237,81 @@ namespace Flow.Launcher.Core.Resource
return string.Empty;
}
+ #endregion
+
+ #region Load & Change
+
+ public List LoadAvailableThemes()
+ {
+ List themes = new List();
+ foreach (var themeDirectory in _themeDirectories)
+ {
+ var filePaths = Directory
+ .GetFiles(themeDirectory)
+ .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
+ .Select(GetThemeDataFromPath);
+ themes.AddRange(filePaths);
+ }
+
+ return themes.OrderBy(o => o.Name).ToList();
+ }
+
+ public bool ChangeTheme(string theme = null)
+ {
+ if (string.IsNullOrEmpty(theme))
+ theme = GetCurrentTheme();
+
+ string path = GetThemePath(theme);
+ try
+ {
+ if (string.IsNullOrEmpty(path))
+ throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
+
+ // reload all resources even if the theme itself hasn't changed in order to pickup changes
+ // to things like fonts
+ UpdateResourceDictionary(GetResourceDictionary(theme));
+
+ _settings.Theme = 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();
+ //if (_settings.UseDropShadowEffect)
+ // AddDropShadowEffectToCurrentTheme();
+ //Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
+ _ = SetBlurForWindowAsync();
+ }
+ catch (DirectoryNotFoundException)
+ {
+ Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
+ if (theme != Constant.DefaultTheme)
+ {
+ _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme));
+ ChangeTheme(Constant.DefaultTheme);
+ }
+ return false;
+ }
+ catch (XamlParseException)
+ {
+ Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
+ if (theme != Constant.DefaultTheme)
+ {
+ _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme));
+ ChangeTheme(Constant.DefaultTheme);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ #endregion
+
+ #region Shadow Effect
+
public void AddDropShadowEffectToCurrentTheme()
{
var dict = GetCurrentResourceDictionary();
@@ -311,8 +330,7 @@ namespace Flow.Launcher.Core.Resource
}
};
- var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
- if (marginSetter == null)
+ if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) is not Setter marginSetter)
{
var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin);
marginSetter = new Setter()
@@ -347,14 +365,12 @@ namespace Flow.Launcher.Core.Resource
var dict = GetCurrentResourceDictionary();
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
- var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter;
- var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
-
- if (effectSetter != null)
+ if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) is Setter effectSetter)
{
windowBorderStyle.Setters.Remove(effectSetter);
}
- if (marginSetter != null)
+
+ if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) is Setter marginSetter)
{
var currentMargin = (Thickness)marginSetter.Value;
var newMargin = new Thickness(
@@ -395,6 +411,343 @@ namespace Flow.Launcher.Core.Resource
}
}
+ #endregion
+
+ #region Blur Handling
+
+ ///
+ /// Refreshes the frame to apply the current theme settings.
+ ///
+ public async Task RefreshFrameAsync()
+ {
+ await Application.Current.Dispatcher.InvokeAsync(() =>
+ {
+ // Get the actual backdrop type and drop shadow effect settings
+ var (backdropType, useDropShadowEffect) = GetActualValue();
+
+ // Remove OS minimizing/maximizing animation
+ // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3);
+
+ // The timing of adding the shadow effect should vary depending on whether the theme is transparent.
+ if (BlurEnabled)
+ {
+ AutoDropShadow(useDropShadowEffect);
+ }
+ SetBlurForWindow(GetCurrentTheme(), backdropType);
+
+ if (!BlurEnabled)
+ {
+ AutoDropShadow(useDropShadowEffect);
+ }
+ }, DispatcherPriority.Normal);
+ }
+
+ ///
+ /// Sets the blur for a window via SetWindowCompositionAttribute
+ ///
+ public async Task SetBlurForWindowAsync()
+ {
+ await Application.Current.Dispatcher.InvokeAsync(() =>
+ {
+ // Get the actual backdrop type and drop shadow effect settings
+ var (backdropType, _) = GetActualValue();
+
+ SetBlurForWindow(GetCurrentTheme(), backdropType);
+ }, DispatcherPriority.Normal);
+ }
+
+ ///
+ /// Gets the actual backdrop type and drop shadow effect settings based on the current theme status.
+ ///
+ public (BackdropTypes BackdropType, bool UseDropShadowEffect) GetActualValue()
+ {
+ var backdropType = _settings.BackdropType;
+ var useDropShadowEffect = _settings.UseDropShadowEffect;
+
+ // When changed non-blur theme, change to backdrop to none
+ if (!BlurEnabled)
+ {
+ backdropType = BackdropTypes.None;
+ }
+
+ // Dropshadow on and control disabled.(user can't change dropshadow with blur theme)
+ if (BlurEnabled)
+ {
+ useDropShadowEffect = true;
+ }
+
+ return (backdropType, useDropShadowEffect);
+ }
+
+ private void SetBlurForWindow(string theme, BackdropTypes backdropType)
+ {
+ var dict = GetThemeResourceDictionary(theme);
+ if (dict == null)
+ return;
+
+ var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null;
+ if (windowBorderStyle == null)
+ return;
+
+ Window mainWindow = Application.Current.MainWindow;
+ if (mainWindow == null)
+ return;
+
+ // Check if the theme supports blur
+ bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;
+ 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))));
+ }
+ 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)));
+ }
+
+ // Apply the blur effect
+ Win32Helper.DWMSetBackdropForWindow(mainWindow, backdropType);
+ ColorizeWindow(theme, backdropType);
+ }
+ else
+ {
+ // Apply default style when Blur is disabled
+ Win32Helper.DWMSetBackdropForWindow(mainWindow, BackdropTypes.None);
+ ColorizeWindow(theme, backdropType);
+ }
+
+ UpdateResourceDictionary(dict);
+ }
+
+ private void AutoDropShadow(bool useDropShadowEffect)
+ {
+ SetWindowCornerPreference("Default");
+ RemoveDropShadowEffectFromCurrentTheme();
+ if (useDropShadowEffect)
+ {
+ if (BlurEnabled && Win32Helper.IsBackdropSupported())
+ {
+ SetWindowCornerPreference("Round");
+ }
+ else
+ {
+ SetWindowCornerPreference("Default");
+ AddDropShadowEffectToCurrentTheme();
+ }
+ }
+ else
+ {
+ if (BlurEnabled && Win32Helper.IsBackdropSupported())
+ {
+ SetWindowCornerPreference("Default");
+ }
+ else
+ {
+ RemoveDropShadowEffectFromCurrentTheme();
+ }
+ }
+ }
+
+ private static void SetWindowCornerPreference(string cornerType)
+ {
+ Window mainWindow = Application.Current.MainWindow;
+ if (mainWindow == null)
+ return;
+
+ Win32Helper.DWMSetCornerPreferenceForWindow(mainWindow, cornerType);
+ }
+
+ // Get Background Color from WindowBorderStyle when there not color for BG.
+ // for theme has not "LightBG" or "DarkBG" case.
+ private Color GetWindowBorderStyleBackground(string theme)
+ {
+ var Resources = GetThemeResourceDictionary(theme);
+ var windowBorderStyle = (Style)Resources["WindowBorderStyle"];
+
+ var backgroundSetter = windowBorderStyle.Setters
+ .OfType()
+ .FirstOrDefault(s => s.Property == Border.BackgroundProperty);
+
+ if (backgroundSetter != null)
+ {
+ // Background's Value is DynamicColor Case
+ var backgroundValue = backgroundSetter.Value;
+
+ if (backgroundValue is SolidColorBrush solidColorBrush)
+ {
+ return solidColorBrush.Color; // Return SolidColorBrush's Color
+ }
+ else if (backgroundValue is DynamicResourceExtension dynamicResource)
+ {
+ // When DynamicResource Extension it is, Key is resource's name.
+ var resourceKey = backgroundSetter.Value.ToString();
+
+ // find key in resource and return color.
+ if (Resources.Contains(resourceKey))
+ {
+ var colorResource = Resources[resourceKey];
+ if (colorResource is SolidColorBrush colorBrush)
+ {
+ return colorBrush.Color;
+ }
+ else if (colorResource is Color color)
+ {
+ return color;
+ }
+ }
+ }
+ }
+
+ return Colors.Transparent; // Default is transparent
+ }
+
+ private void ApplyPreviewBackground(Color? bgColor = null)
+ {
+ if (bgColor == null) return;
+
+ // Copy the existing WindowBorderStyle
+ var previewStyle = new Style(typeof(Border));
+ if (Application.Current.Resources.Contains("WindowBorderStyle"))
+ {
+ if (Application.Current.Resources["WindowBorderStyle"] is Style originalStyle)
+ {
+ foreach (var setter in originalStyle.Setters.OfType())
+ {
+ previewStyle.Setters.Add(new Setter(setter.Property, setter.Value));
+ }
+ }
+ }
+
+ // Apply background color (remove transparency in color)
+ // WPF does not allow the use of an acrylic brush within the window's internal area,
+ // so transparency effects are not applied to the preview.
+ Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B);
+ previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor)));
+
+ // 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.
+ if (BlurEnabled)
+ {
+ previewStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5)));
+ previewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(1)));
+ }
+ Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle;
+ }
+
+ private void ColorizeWindow(string theme, BackdropTypes backdropType)
+ {
+ var dict = GetThemeResourceDictionary(theme);
+ if (dict == null) return;
+
+ var mainWindow = Application.Current.MainWindow;
+ if (mainWindow == null) return;
+
+ // Check if the theme supports blur
+ bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;
+
+ // SystemBG value check (Auto, Light, Dark)
+ string 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;
+
+ // Final decision on whether to use dark mode
+ bool useDarkMode = false;
+
+ // If systemBG is not "Auto", prioritize it over ColorScheme and set the mode based on systemBG value
+ if (systemBG == "Dark")
+ {
+ useDarkMode = true; // Dark
+ }
+ else if (systemBG == "Light")
+ {
+ useDarkMode = false; // Light
+ }
+ else if (systemBG == "Auto")
+ {
+ // If systemBG is "Auto", decide based on ColorScheme
+ if (colorScheme == "Dark")
+ useDarkMode = true;
+ else if (colorScheme == "Light")
+ useDarkMode = false;
+ else
+ useDarkMode = isSystemDark; // Auto (based on system setting)
+ }
+
+ // Apply DWM Dark Mode
+ Win32Helper.DWMSetDarkModeForWindow(mainWindow, useDarkMode);
+
+ Color LightBG;
+ Color DarkBG;
+
+ // Retrieve LightBG value (fallback to WindowBorderStyle background color if not found)
+ try
+ {
+ LightBG = dict.Contains("LightBG") ? (Color)dict["LightBG"] : GetWindowBorderStyleBackground(theme);
+ }
+ catch (Exception)
+ {
+ LightBG = GetWindowBorderStyleBackground(theme);
+ }
+
+ // Retrieve DarkBG value (fallback to LightBG if not found)
+ try
+ {
+ DarkBG = dict.Contains("DarkBG") ? (Color)dict["DarkBG"] : LightBG;
+ }
+ catch (Exception)
+ {
+ DarkBG = LightBG;
+ }
+
+ // Select background color based on ColorScheme and SystemBG
+ Color selectedBG = useDarkMode ? DarkBG : LightBG;
+ ApplyPreviewBackground(selectedBG);
+
+ bool isBlurAvailable = hasBlur && Win32Helper.IsBackdropSupported(); // Windows 11 미만이면 hasBlur를 강제 false
+
+ if (!isBlurAvailable)
+ {
+ mainWindow.Background = Brushes.Transparent;
+ }
+ else
+ {
+ // 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));
+ }
+ else
+ {
+ mainWindow.Background = new SolidColorBrush(selectedBG);
+ }
+ }
+ }
+
+ private static bool IsBlurTheme()
+ {
+ if (!Win32Helper.IsBackdropSupported()) // Windows 11 미만이면 무조건 false
+ return false;
+
+ var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
+
+ return resource is bool b && b;
+ }
+
+ #endregion
+
+ #region Classes
+
public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null);
+
+ #endregion
}
}
diff --git a/Flow.Launcher.Infrastructure/MonitorInfo.cs b/Flow.Launcher.Infrastructure/MonitorInfo.cs
new file mode 100644
index 000000000..3221708c1
--- /dev/null
+++ b/Flow.Launcher.Infrastructure/MonitorInfo.cs
@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+using System;
+using System.Runtime.InteropServices;
+using System.Windows;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Graphics.Gdi;
+using Windows.Win32.UI.WindowsAndMessaging;
+
+namespace Flow.Launcher.Infrastructure;
+
+///
+/// Contains full information about a display monitor.
+/// Codes are edited from: .
+///
+internal class MonitorInfo
+{
+ ///
+ /// Gets the display monitors (including invisible pseudo-monitors associated with the mirroring drivers).
+ ///
+ /// A list of display monitors
+ public static unsafe IList GetDisplayMonitors()
+ {
+ var monitorCount = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CMONITORS);
+ var list = new List(monitorCount);
+ var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) =>
+ {
+ list.Add(new MonitorInfo(monitor, rect));
+ return true;
+ });
+ var dwData = new LPARAM();
+ var hdc = new HDC();
+ bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData);
+ if (!ok)
+ {
+ Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
+ }
+ return list;
+ }
+
+ ///
+ /// Gets the display monitor that is nearest to a given window.
+ ///
+ /// Window handle
+ /// The display monitor that is nearest to a given window, or null if no monitor is found.
+ public static unsafe MonitorInfo GetNearestDisplayMonitor(HWND hwnd)
+ {
+ var nearestMonitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
+ MonitorInfo nearestMonitorInfo = null;
+ var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) =>
+ {
+ if (monitor == nearestMonitor)
+ {
+ nearestMonitorInfo = new MonitorInfo(monitor, rect);
+ return false;
+ }
+ return true;
+ });
+ var dwData = new LPARAM();
+ var hdc = new HDC();
+ bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData);
+ if (!ok)
+ {
+ Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
+ }
+ return nearestMonitorInfo;
+ }
+
+ private readonly HMONITOR _monitor;
+
+ internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect)
+ {
+ RectMonitor =
+ new Rect(new Point(rect->left, rect->top),
+ new Point(rect->right, rect->bottom));
+ _monitor = monitor;
+ var info = new MONITORINFOEXW() { monitorInfo = new MONITORINFO() { cbSize = (uint)sizeof(MONITORINFOEXW) } };
+ GetMonitorInfo(monitor, ref info);
+ RectWork =
+ new Rect(new Point(info.monitorInfo.rcWork.left, info.monitorInfo.rcWork.top),
+ new Point(info.monitorInfo.rcWork.right, info.monitorInfo.rcWork.bottom));
+ Name = new string(info.szDevice.AsSpan()).Replace("\0", "").Trim();
+ }
+
+ ///
+ /// Gets the name of the display.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the display monitor rectangle, expressed in virtual-screen coordinates.
+ ///
+ ///
+ /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.
+ ///
+ public Rect RectMonitor { get; }
+
+ ///
+ /// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates.
+ ///
+ ///
+ /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.
+ ///
+ public Rect RectWork { get; }
+
+ ///
+ /// Gets if the monitor is the the primary display monitor.
+ ///
+ public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(IntPtr.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
+
+ ///
+ public override string ToString() => $"{Name} {RectMonitor.Width}x{RectMonitor.Height}";
+
+ private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi)
+ {
+ fixed (MONITORINFOEXW* lpmiLocal = &lpmi)
+ {
+ var lpmiBase = (MONITORINFO*)lpmiLocal;
+ var __result = PInvoke.GetMonitorInfo(hMonitor, lpmiBase);
+ return __result;
+ }
+ }
+}
diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt
index f117534a1..f080f24de 100644
--- a/Flow.Launcher.Infrastructure/NativeMethods.txt
+++ b/Flow.Launcher.Infrastructure/NativeMethods.txt
@@ -16,4 +16,34 @@ WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
-EnumWindows
\ No newline at end of file
+EnumWindows
+
+DwmSetWindowAttribute
+DWM_SYSTEMBACKDROP_TYPE
+DWM_WINDOW_CORNER_PREFERENCE
+
+MAX_PATH
+SystemParametersInfo
+
+SetForegroundWindow
+
+GetWindowLong
+GetForegroundWindow
+GetDesktopWindow
+GetShellWindow
+GetWindowRect
+GetClassName
+FindWindowEx
+WINDOW_STYLE
+
+SetLastError
+WINDOW_EX_STYLE
+
+GetSystemMetrics
+EnumDisplayMonitors
+MonitorFromWindow
+GetMonitorInfo
+MONITORINFOEXW
+
+WM_ENTERSIZEMOVE
+WM_EXITSIZEMOVE
\ No newline at end of file
diff --git a/Flow.Launcher.Infrastructure/PInvokeExtensions.cs b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs
new file mode 100644
index 000000000..1a72ab7a6
--- /dev/null
+++ b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs
@@ -0,0 +1,25 @@
+using System.Runtime.InteropServices;
+using Windows.Win32.Foundation;
+using Windows.Win32.UI.WindowsAndMessaging;
+
+namespace Windows.Win32;
+
+// Edited from: https://github.com/files-community/Files
+internal static partial class PInvoke
+{
+ [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)]
+ static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong);
+
+ [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)]
+ static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong);
+
+ // NOTE:
+ // CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa.
+ // For more info, visit https://github.com/microsoft/CsWin32/issues/882
+ public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong)
+ {
+ return sizeof(nint) is 4
+ ? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong)
+ : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong);
+ }
+}
diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 93f6db111..63debfb47 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -76,6 +76,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
}
public bool UseDropShadowEffect { get; set; } = true;
+ public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None;
/* Appearance Settings. It should be separated from the setting later.*/
public double WindowHeightSize { get; set; } = 42;
@@ -430,4 +431,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
Fast,
Custom
}
+
+ public enum BackdropTypes
+ {
+ None,
+ Acrylic,
+ Mica,
+ MicaAlt
+ }
}
diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs
index 867fef4f5..8dbe3f7e9 100644
--- a/Flow.Launcher.Infrastructure/Win32Helper.cs
+++ b/Flow.Launcher.Infrastructure/Win32Helper.cs
@@ -1,7 +1,14 @@
using System;
+using System.ComponentModel;
using System.Runtime.InteropServices;
-using System.Windows.Interop;
using System.Windows;
+using System.Windows.Interop;
+using System.Windows.Media;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Graphics.Dwm;
+using Windows.Win32.UI.WindowsAndMessaging;
+using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher.Infrastructure
{
@@ -9,93 +16,306 @@ namespace Flow.Launcher.Infrastructure
{
#region Blur Handling
- /*
- Found on https://github.com/riverar/sample-win10-aeroglass
- */
-
- private enum AccentState
+ public static bool IsBackdropSupported()
{
- ACCENT_DISABLED = 0,
- ACCENT_ENABLE_GRADIENT = 1,
- ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
- ACCENT_ENABLE_BLURBEHIND = 3,
- ACCENT_INVALID_STATE = 4
+ // Mica and Acrylic only supported Windows 11 22000+
+ return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
+ Environment.OSVersion.Version.Build >= 22000;
}
- [StructLayout(LayoutKind.Sequential)]
- private struct AccentPolicy
+ public static unsafe bool DWMSetCloakForWindow(Window window, bool cloak)
{
- public AccentState AccentState;
- public int AccentFlags;
- public int GradientColor;
- public int AnimationId;
+ var cloaked = cloak ? 1 : 0;
+
+ return PInvoke.DwmSetWindowAttribute(
+ GetWindowHandle(window),
+ DWMWINDOWATTRIBUTE.DWMWA_CLOAK,
+ &cloaked,
+ (uint)Marshal.SizeOf()).Succeeded;
}
- [StructLayout(LayoutKind.Sequential)]
- private struct WindowCompositionAttributeData
+ public static unsafe bool DWMSetBackdropForWindow(Window window, BackdropTypes backdrop)
{
- public WindowCompositionAttribute Attribute;
- public IntPtr Data;
- public int SizeOfData;
+ var backdropType = backdrop switch
+ {
+ BackdropTypes.Acrylic => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TRANSIENTWINDOW,
+ BackdropTypes.Mica => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_MAINWINDOW,
+ BackdropTypes.MicaAlt => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TABBEDWINDOW,
+ _ => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_AUTO
+ };
+
+ return PInvoke.DwmSetWindowAttribute(
+ GetWindowHandle(window),
+ DWMWINDOWATTRIBUTE.DWMWA_SYSTEMBACKDROP_TYPE,
+ &backdropType,
+ (uint)Marshal.SizeOf()).Succeeded;
}
- private enum WindowCompositionAttribute
+ public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMode)
{
- WCA_ACCENT_POLICY = 19
- }
+ var darkMode = useDarkMode ? 1 : 0;
- [DllImport("user32.dll")]
- private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
+ return PInvoke.DwmSetWindowAttribute(
+ GetWindowHandle(window),
+ DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE,
+ &darkMode,
+ (uint)Marshal.SizeOf()).Succeeded;
+ }
///
- /// Checks if the blur theme is enabled
+ ///
///
- public static bool IsBlurTheme()
+ ///
+ /// DoNotRound, Round, RoundSmall, Default
+ ///
+ public static unsafe bool DWMSetCornerPreferenceForWindow(Window window, string cornerType)
{
- if (Environment.OSVersion.Version >= new Version(6, 2))
+ var preference = cornerType switch
{
- var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
+ "DoNotRound" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DONOTROUND,
+ "Round" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND,
+ "RoundSmall" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUNDSMALL,
+ "Default" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DEFAULT,
+ _ => throw new InvalidOperationException("Invalid corner type")
+ };
- if (resource is bool b)
- return b;
+ return PInvoke.DwmSetWindowAttribute(
+ GetWindowHandle(window),
+ DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE,
+ &preference,
+ (uint)Marshal.SizeOf()).Succeeded;
+ }
+ #endregion
+
+ #region Wallpaper
+
+ public static unsafe string GetWallpaperPath()
+ {
+ var wallpaperPtr = stackalloc char[(int)PInvoke.MAX_PATH];
+ PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, PInvoke.MAX_PATH,
+ wallpaperPtr,
+ 0);
+ var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr);
+
+ return wallpaper.ToString();
+ }
+
+ #endregion
+
+ #region Window Foreground
+
+ public static nint GetForegroundWindow()
+ {
+ return PInvoke.GetForegroundWindow().Value;
+ }
+
+ public static bool SetForegroundWindow(Window window)
+ {
+ return PInvoke.SetForegroundWindow(GetWindowHandle(window));
+ }
+
+ public static bool SetForegroundWindow(nint handle)
+ {
+ return PInvoke.SetForegroundWindow(new(handle));
+ }
+
+ #endregion
+
+ #region Task Switching
+
+ ///
+ /// Hide windows in the Alt+Tab window list
+ ///
+ /// To hide a window
+ public static void HideFromAltTab(Window window)
+ {
+ var hwnd = GetWindowHandle(window);
+
+ var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
+
+ // Add TOOLWINDOW style, remove APPWINDOW style
+ var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
+
+ SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
+ }
+
+ ///
+ /// Restore window display in the Alt+Tab window list.
+ ///
+ /// To restore the displayed window
+ public static void ShowInAltTab(Window window)
+ {
+ var hwnd = GetWindowHandle(window);
+
+ var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
+
+ // Remove the TOOLWINDOW style and add the APPWINDOW style.
+ var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
+
+ SetWindowStyle(GetWindowHandle(window), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
+ }
+
+ ///
+ /// Disable windows toolbar's control box
+ /// This will also disable system menu with Alt+Space hotkey
+ ///
+ public static void DisableControlBox(Window window)
+ {
+ var hwnd = GetWindowHandle(window);
+
+ var style = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
+
+ style &= ~(int)WINDOW_STYLE.WS_SYSMENU;
+
+ SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style);
+ }
+
+ private static int GetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex)
+ {
+ var style = PInvoke.GetWindowLong(hWnd, nIndex);
+ if (style == 0 && Marshal.GetLastPInvokeError() != 0)
+ {
+ throw new Win32Exception(Marshal.GetLastPInvokeError());
+ }
+ return style;
+ }
+
+ private static nint SetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong)
+ {
+ PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error
+
+ var result = PInvoke.SetWindowLongPtr(hWnd, nIndex, dwNewLong);
+ if (result == 0 && Marshal.GetLastPInvokeError() != 0)
+ {
+ throw new Win32Exception(Marshal.GetLastPInvokeError());
+ }
+
+ return result;
+ }
+
+ #endregion
+
+ #region Window Fullscreen
+
+ private const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
+ private const string WINDOW_CLASS_WINTAB = "Flip3D";
+ private const string WINDOW_CLASS_PROGMAN = "Progman";
+ private const string WINDOW_CLASS_WORKERW = "WorkerW";
+
+ private static HWND _hwnd_shell;
+ private static HWND HWND_SHELL =>
+ _hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow();
+
+ private static HWND _hwnd_desktop;
+ private static HWND HWND_DESKTOP =>
+ _hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow();
+
+ public static unsafe bool IsForegroundWindowFullscreen()
+ {
+ // Get current active window
+ var hWnd = PInvoke.GetForegroundWindow();
+ if (hWnd.Equals(HWND.Null))
+ {
return false;
}
- return false;
+ // If current active window is desktop or shell, exit early
+ if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL))
+ {
+ return false;
+ }
+
+ string windowClass;
+ const int capacity = 256;
+ Span buffer = stackalloc char[capacity];
+ int validLength;
+ fixed (char* pBuffer = buffer)
+ {
+ validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity);
+ }
+
+ windowClass = buffer[..validLength].ToString();
+
+ // For Win+Tab (Flip3D)
+ if (windowClass == WINDOW_CLASS_WINTAB)
+ {
+ return false;
+ }
+
+ PInvoke.GetWindowRect(hWnd, out var appBounds);
+
+ // For console (ConsoleWindowClass), we have to check for negative dimensions
+ if (windowClass == WINDOW_CLASS_CONSOLE)
+ {
+ return appBounds.top < 0 && appBounds.bottom < 0;
+ }
+
+ // For desktop (Progman or WorkerW, depends on the system), we have to check
+ if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW)
+ {
+ var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null);
+ hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView");
+ if (hWndDesktop.Value != IntPtr.Zero)
+ {
+ return false;
+ }
+ }
+
+ var monitorInfo = MonitorInfo.GetNearestDisplayMonitor(hWnd);
+ return (appBounds.bottom - appBounds.top) == monitorInfo.RectMonitor.Height &&
+ (appBounds.right - appBounds.left) == monitorInfo.RectMonitor.Width;
}
+ #endregion
+
+ #region Pixel to DIP
+
///
- /// Sets the blur for a window via SetWindowCompositionAttribute
+ /// Transforms pixels to Device Independent Pixels used by WPF
///
- public static void SetBlurForWindow(Window w, bool blur)
+ /// current window, required to get presentation source
+ /// horizontal position in pixels
+ /// vertical position in pixels
+ /// point containing device independent pixels
+ public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY)
{
- SetWindowAccent(w, blur ? AccentState.ACCENT_ENABLE_BLURBEHIND : AccentState.ACCENT_DISABLED);
- }
-
- private static void SetWindowAccent(Window w, AccentState state)
- {
- var windowHelper = new WindowInteropHelper(w);
-
- windowHelper.EnsureHandle();
-
- var accent = new AccentPolicy { AccentState = state };
- var accentStructSize = Marshal.SizeOf(accent);
-
- var accentPtr = Marshal.AllocHGlobal(accentStructSize);
- Marshal.StructureToPtr(accent, accentPtr, false);
-
- var data = new WindowCompositionAttributeData
+ Matrix matrix;
+ var source = PresentationSource.FromVisual(visual);
+ if (source is not null)
{
- Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
- SizeOfData = accentStructSize,
- Data = accentPtr
- };
+ matrix = source.CompositionTarget.TransformFromDevice;
+ }
+ else
+ {
+ using var src = new HwndSource(new HwndSourceParameters());
+ matrix = src.CompositionTarget.TransformFromDevice;
+ }
- SetWindowCompositionAttribute(windowHelper.Handle, ref data);
-
- Marshal.FreeHGlobal(accentPtr);
+ return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
}
+
+ #endregion
+
+ #region WndProc
+
+ public const int WM_ENTERSIZEMOVE = (int)PInvoke.WM_ENTERSIZEMOVE;
+ public const int WM_EXITSIZEMOVE = (int)PInvoke.WM_EXITSIZEMOVE;
+
+ #endregion
+
+ #region Window Handle
+
+ internal static HWND GetWindowHandle(Window window, bool ensure = false)
+ {
+ var windowHelper = new WindowInteropHelper(window);
+ if (ensure)
+ {
+ windowHelper.EnsureHandle();
+ }
+ return new(windowHelper.Handle);
+ }
+
#endregion
}
}
diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
index 4c7af4cd4..f178ebb90 100644
--- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
@@ -152,7 +152,6 @@ namespace Flow.Launcher.Plugin
///
public void RemoveGlobalKeyboardCallback(Func callback);
-
///
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
///
@@ -191,14 +190,15 @@ namespace Flow.Launcher.Plugin
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default);
///
- /// Add ActionKeyword for specific plugin
+ /// Add ActionKeyword and update action keyword metadata for specific plugin
+ /// Before adding, please check if action keyword is already assigned by
///
/// ID for plugin that needs to add action keyword
/// The actionkeyword that is supposed to be added
void AddActionKeyword(string pluginId, string newActionKeyword);
///
- /// Remove ActionKeyword for specific plugin
+ /// Remove ActionKeyword and update action keyword metadata for specific plugin
///
/// ID for plugin that needs to remove action keyword
/// The actionkeyword that is supposed to be removed
diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs
index 259716ec1..42c90bda4 100644
--- a/Flow.Launcher.Plugin/PluginMetadata.cs
+++ b/Flow.Launcher.Plugin/PluginMetadata.cs
@@ -92,6 +92,11 @@ namespace Flow.Launcher.Plugin
/// All action keywords of plugin.
///
public List ActionKeywords { get; set; }
+
+ ///
+ /// Hide plugin keyword setting panel.
+ ///
+ public bool HideActionKeywordPanel { get; set; }
///
/// Plugin icon path.
diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs
index a9694e263..913dc31ae 100644
--- a/Flow.Launcher.Plugin/Query.cs
+++ b/Flow.Launcher.Plugin/Query.cs
@@ -40,10 +40,9 @@ namespace Flow.Launcher.Plugin
public const string TermSeparator = " ";
///
- /// User can set multiple action keywords seperated by ';'
+ /// User can set multiple action keywords seperated by whitespace
///
- public const string ActionKeywordSeparator = ";";
-
+ public const string ActionKeywordSeparator = TermSeparator;
///
/// Wildcard action keyword. Plugins using this value will be queried on every search.
diff --git a/Flow.Launcher/ActionKeywords.xaml.cs b/Flow.Launcher/ActionKeywords.xaml.cs
index c3966e618..d403d4847 100644
--- a/Flow.Launcher/ActionKeywords.xaml.cs
+++ b/Flow.Launcher/ActionKeywords.xaml.cs
@@ -3,6 +3,8 @@ using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
using Flow.Launcher.Core;
+using System.Linq;
+using System.Collections.Generic;
namespace Flow.Launcher
{
@@ -32,14 +34,37 @@ namespace Flow.Launcher
private void btnDone_OnClick(object sender, RoutedEventArgs _)
{
- var oldActionKeyword = plugin.Metadata.ActionKeywords[0];
- var newActionKeyword = tbAction.Text.Trim();
- newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*";
-
- if (!PluginViewModel.IsActionKeywordRegistered(newActionKeyword))
+ var oldActionKeywords = plugin.Metadata.ActionKeywords;
+
+ var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList();
+ newActionKeywords.RemoveAll(string.IsNullOrEmpty);
+ newActionKeywords = newActionKeywords.Distinct().ToList();
+
+ newActionKeywords = newActionKeywords.Count > 0 ? newActionKeywords : new() { Query.GlobalPluginWildcardSign };
+
+ var addedActionKeywords = newActionKeywords.Except(oldActionKeywords).ToList();
+ var removedActionKeywords = oldActionKeywords.Except(newActionKeywords).ToList();
+ if (!addedActionKeywords.Any(App.API.ActionKeywordAssigned))
{
- pluginViewModel.ChangeActionKeyword(newActionKeyword, oldActionKeyword);
- Close();
+ if (oldActionKeywords.Count != newActionKeywords.Count)
+ {
+ ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
+ return;
+ }
+
+ var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList();
+ var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList();
+
+ if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
+ {
+ // User just changes the sequence of action keywords
+ var msg = translater.GetTranslation("newActionKeywordsSameAsOld");
+ MessageBoxEx.Show(msg);
+ }
+ else
+ {
+ ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
+ }
}
else
{
@@ -47,5 +72,21 @@ namespace Flow.Launcher
App.API.ShowMsgBox(msg);
}
}
+
+ private void ReplaceActionKeyword(string id, IReadOnlyList removedActionKeywords, IReadOnlyList addedActionKeywords)
+ {
+ foreach (var actionKeyword in removedActionKeywords)
+ {
+ App.API.RemoveActionKeyword(id, actionKeyword);
+ }
+ foreach (var actionKeyword in addedActionKeywords)
+ {
+ App.API.AddActionKeyword(id, actionKeyword);
+ }
+
+ // Update action keywords text and close window
+ pluginViewModel.OnActionKeywordsChanged();
+ Close();
+ }
}
}
diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml
index 17c0ae0d5..565bbe3c7 100644
--- a/Flow.Launcher/App.xaml
+++ b/Flow.Launcher/App.xaml
@@ -4,7 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.modernwpf.com/2019"
ShutdownMode="OnMainWindowClose"
- Startup="OnStartupAsync">
+ Startup="OnStartup">
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index 550b1bdae..23c77618f 100644
--- a/Flow.Launcher/App.xaml.cs
+++ b/Flow.Launcher/App.xaml.cs
@@ -101,15 +101,15 @@ namespace Flow.Launcher
{
if (SingleInstance.InitializeAsFirstInstance(Unique))
{
- using (var application = new App())
- {
- application.InitializeComponent();
- application.Run();
- }
+ using var application = new App();
+ application.InitializeComponent();
+ application.Run();
}
}
- private async void OnStartupAsync(object sender, StartupEventArgs e)
+#pragma warning disable VSTHRD100 // Avoid async void methods
+
+ private async void OnStartup(object sender, StartupEventArgs e)
{
await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
{
@@ -128,7 +128,7 @@ namespace Flow.Launcher
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
- InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
+ Ioc.Default.GetRequiredService().ChangeLanguage(_settings.Language);
PluginManager.LoadPlugins(_settings.PluginSettings);
@@ -137,8 +137,7 @@ namespace Flow.Launcher
await PluginManager.InitializePluginsAsync();
await imageLoadertask;
- var mainVM = Ioc.Default.GetRequiredService();
- var window = new MainWindow(_settings, mainVM);
+ var window = new MainWindow();
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
@@ -149,7 +148,7 @@ namespace Flow.Launcher
// main windows needs initialized before theme change because of blur settings
// TODO: Clean ThemeManager.Instance in future
- ThemeManager.Instance.ChangeTheme(_settings.Theme);
+ Ioc.Default.GetRequiredService().ChangeTheme();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
@@ -164,6 +163,8 @@ namespace Flow.Launcher
});
}
+#pragma warning restore VSTHRD100 // Avoid async void methods
+
private void AutoStartup()
{
// we try to enable auto-startup on first launch, or reenable if it was removed
@@ -186,8 +187,7 @@ namespace Flow.Launcher
// but if it fails (permissions, etc) then don't keep retrying
// this also gives the user a visual indication in the Settings widget
_settings.StartFlowLauncherOnSystemStartup = false;
- Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
- e.Message);
+ API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message);
}
}
}
diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml
index ccf4de870..0171e6d79 100644
--- a/Flow.Launcher/CustomQueryHotkeySetting.xaml
+++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml
@@ -58,9 +58,9 @@
-
+
-
+
@@ -99,11 +99,12 @@
Grid.Row="0"
Grid.Column="1"
Grid.ColumnSpan="2"
- Margin="10,0,10,0"
+ Margin="10 0 10 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
HorizontalContentAlignment="Left"
- DefaultHotkey="" />
+ DefaultHotkey=""
+ Type="CustomQueryHotkey" />
@@ -132,21 +133,21 @@
+ BorderThickness="0 1 0 0">
\ No newline at end of file
diff --git a/Flow.Launcher/Themes/Ubuntu.xaml b/Flow.Launcher/Themes/Ubuntu.xaml
index 3c9e0a125..4007370f9 100644
--- a/Flow.Launcher/Themes/Ubuntu.xaml
+++ b/Flow.Launcher/Themes/Ubuntu.xaml
@@ -32,7 +32,7 @@
x:Key="QueryBoxStyle"
BasedOn="{StaticResource BaseQueryBoxStyle}"
TargetType="{x:Type TextBox}">
-
+
@@ -43,7 +43,7 @@
x:Key="QuerySuggestionBoxStyle"
BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}"
TargetType="{x:Type TextBox}">
-
+
@@ -56,15 +56,7 @@
TargetType="{x:Type Border}">
-
-
-
-
-
-
-
-
-
+
@@ -96,7 +88,7 @@
TargetType="{x:Type Rectangle}">
-
+
-
+
@@ -174,7 +166,7 @@
x:Key="ClockPanel"
BasedOn="{StaticResource ClockPanel}"
TargetType="{x:Type StackPanel}">
-
+