diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs index 8412ba7e8..944b2fd10 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -1,18 +1,14 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Text.Json; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; -using System.Windows.Forms; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; -using CheckBox = System.Windows.Controls.CheckBox; -using ComboBox = System.Windows.Controls.ComboBox; -using Control = System.Windows.Controls.Control; -using Orientation = System.Windows.Controls.Orientation; -using TextBox = System.Windows.Controls.TextBox; -using UserControl = System.Windows.Controls.UserControl; + +#nullable enable namespace Flow.Launcher.Core.Plugin { @@ -23,51 +19,72 @@ namespace Flow.Launcher.Core.Plugin public required string SettingPath { get; init; } public Dictionary SettingControls { get; } = new(); - public IReadOnlyDictionary Inner => Settings; - protected ConcurrentDictionary Settings { get; set; } + public IReadOnlyDictionary Inner => Settings; + protected ConcurrentDictionary Settings { get; set; } = null!; public required IPublicAPI API { get; init; } - private JsonStorage> _storage; + private JsonStorage> _storage = null!; - // maybe move to resource? - private static readonly Thickness settingControlMargin = new(0, 9, 18, 9); - private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9); - private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingDescMargin = new(0, 2, 0, 0); - private static readonly Thickness settingSepMargin = new(0, 0, 0, 2); + private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin"); + private static readonly Thickness SettingPanelItemLeftMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftMargin"); + private static readonly Thickness SettingPanelItemTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemTopBottomMargin"); + private static readonly Thickness SettingPanelItemLeftTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftTopBottomMargin"); + private static readonly double SettingPanelTextBoxMinWidth = (double)Application.Current.FindResource("SettingPanelTextBoxMinWidth"); + private static readonly double SettingPanelPathTextBoxWidth = (double)Application.Current.FindResource("SettingPanelPathTextBoxWidth"); + private static readonly double SettingPanelAreaTextBoxMinHeight = (double)Application.Current.FindResource("SettingPanelAreaTextBoxMinHeight"); public async Task InitializeAsync() { - _storage = new JsonStorage>(SettingPath); - Settings = await _storage.LoadAsync(); - - if (Configuration == null) + if (Settings == null) { - return; + _storage = new JsonStorage>(SettingPath); + Settings = await _storage.LoadAsync(); + + // Because value type of settings dictionary is object which causes them to be JsonElement when loading from json files, + // we need to convert it to the correct type + foreach (var (key, value) in Settings) + { + if (value is not JsonElement jsonElement) continue; + + Settings[key] = jsonElement.ValueKind switch + { + JsonValueKind.String => jsonElement.GetString() ?? value, + JsonValueKind.True => jsonElement.GetBoolean(), + JsonValueKind.False => jsonElement.GetBoolean(), + JsonValueKind.Null => null, + _ => value + }; + } } + if (Configuration == null) return; + foreach (var (type, attributes) in Configuration.Body) { - if (attributes.Name == null) - { - continue; - } + // Skip if the setting does not have attributes or name + if (attributes?.Name == null) continue; - if (!Settings.ContainsKey(attributes.Name)) + // Skip if the setting does not have attributes or name + if (!NeedSaveInSettings(type)) continue; + + // If need save in settings, we need to make sure the setting exists in the settings file + if (Settings.ContainsKey(attributes.Name)) continue; + + if (type == "checkbox") + { + // If can parse the default value to bool, use it, otherwise use false + Settings[attributes.Name] = bool.TryParse(attributes.DefaultValue, out var value) && value; + } + else { Settings[attributes.Name] = attributes.DefaultValue; } } } - public void UpdateSettings(IReadOnlyDictionary settings) { - if (settings == null || settings.Count == 0) - return; + if (settings == null || settings.Count == 0) return; foreach (var (key, value) in settings) { @@ -78,19 +95,23 @@ namespace Flow.Launcher.Core.Plugin switch (control) { case TextBox textBox: - textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty); + var text = value as string ?? string.Empty; + textBox.Dispatcher.Invoke(() => textBox.Text = text); break; case PasswordBox passwordBox: - passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty); + var password = value as string ?? string.Empty; + passwordBox.Dispatcher.Invoke(() => passwordBox.Password = password); break; case ComboBox comboBox: comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value); break; case CheckBox checkBox: - checkBox.Dispatcher.Invoke(() => - checkBox.IsChecked = value is bool isChecked - ? isChecked - : bool.Parse(value as string ?? string.Empty)); + var isChecked = value is bool boolValue + ? boolValue + // If can parse the default value to bool, use it, otherwise use false + : value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString) + && boolValueFromString; + checkBox.Dispatcher.Invoke(() =>checkBox.IsChecked = isChecked); break; } } @@ -108,7 +129,7 @@ namespace Flow.Launcher.Core.Plugin { _storage.Save(); } - + public bool NeedCreateSettingPanel() { // If there are no settings or the settings configuration is empty, return null @@ -118,329 +139,350 @@ namespace Flow.Launcher.Core.Plugin public Control CreateSettingPanel() { // No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true + // if (!NeedCreateSettingPanel()) return null; - var settingWindow = new UserControl(); - var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center }; - - ColumnDefinition gridCol1 = new ColumnDefinition(); - ColumnDefinition gridCol2 = new ColumnDefinition(); - - gridCol1.Width = new GridLength(70, GridUnitType.Star); - gridCol2.Width = new GridLength(30, GridUnitType.Star); - mainPanel.ColumnDefinitions.Add(gridCol1); - mainPanel.ColumnDefinitions.Add(gridCol2); - settingWindow.Content = mainPanel; - int rowCount = 0; - - foreach (var (type, attribute) in Configuration.Body) + // Create main grid with two columns (Column 1: Auto, Column 2: *) + var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center }; + mainPanel.ColumnDefinitions.Add(new ColumnDefinition() { - Separator sep = new Separator(); - sep.VerticalAlignment = VerticalAlignment.Top; - sep.Margin = settingSepMargin; - sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ - var panel = new StackPanel + Width = new GridLength(0, GridUnitType.Auto) + }); + mainPanel.ColumnDefinitions.Add(new ColumnDefinition() + { + Width = new GridLength(1, GridUnitType.Star) + }); + + // Iterate over each setting and create one row for it + var rowCount = 0; + foreach (var (type, attributes) in Configuration!.Body) + { + // Skip if the setting does not have attributes or name + if (attributes?.Name == null) continue; + + // Add a new row to the main grid + mainPanel.RowDefinitions.Add(new RowDefinition() { - Orientation = Orientation.Vertical, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelPanelMargin - }; - - RowDefinition gridRow = new RowDefinition(); - mainPanel.RowDefinitions.Add(gridRow); - var name = new TextBlock() - { - Text = attribute.Label, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - - var desc = new TextBlock() - { - Text = attribute.Description, - FontSize = 12, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingDescMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - - desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); - - if (attribute.Description == null) /* if no description, hide */ - desc.Visibility = Visibility.Collapsed; - - - if (type != "textBlock") /* if textBlock, hide desc */ - { - panel.Children.Add(name); - panel.Children.Add(desc); - } - - - Grid.SetColumn(panel, 0); - Grid.SetRow(panel, rowCount); + Height = new GridLength(0, GridUnitType.Auto) + }); + // State controls for column 0 and 1 + StackPanel? panel = null; FrameworkElement contentControl; + // If the type is textBlock, separator, or checkbox, we do not need to create a panel + if (type != "textBlock" && type != "separator" && type != "checkbox") + { + // Create a panel to hold the label and description + panel = new StackPanel + { + Margin = SettingPanelItemTopBottomMargin, + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Center + }; + + // Create a text block for name + var name = new TextBlock() + { + Text = attributes.Label, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + // Create a text block for description + TextBlock? desc = null; + if (attributes.Description != null) + { + desc = new TextBlock() + { + Text = attributes.Description, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + desc.SetResourceReference(TextBlock.StyleProperty, "SettingPanelTextBlockDescriptionStyle"); // for theme change + } + + // Add the name and description to the panel + panel.Children.Add(name); + if (desc != null) panel.Children.Add(desc); + } + switch (type) { case "textBlock": - { - contentControl = new TextBlock { - Text = attribute.Description.Replace("\\r\\n", "\r\n"), - Margin = settingTextBlockMargin, - Padding = new Thickness(0, 0, 0, 0), - HorizontalAlignment = System.Windows.HorizontalAlignment.Left, - TextAlignment = TextAlignment.Left, - TextWrapping = TextWrapping.Wrap - }; + contentControl = new TextBlock + { + Text = attributes.Description?.Replace("\\r\\n", "\r\n") ?? string.Empty, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemTopBottomMargin, + TextAlignment = TextAlignment.Left, + TextWrapping = TextWrapping.Wrap + }; - Grid.SetColumn(contentControl, 0); - Grid.SetColumnSpan(contentControl, 2); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "input": - { - var textBox = new TextBox() { - Text = Settings[attribute.Name] as string ?? string.Empty, - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; + var textBox = new TextBox() + { + MinWidth = SettingPanelTextBoxMinWidth, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + Text = Settings[attributes.Name] as string ?? string.Empty, + ToolTip = attributes.Description + }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; + textBox.TextChanged += (_, _) => + { + Settings[attributes.Name] = textBox.Text; + }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = textBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "inputWithFileBtn": case "inputWithFolderBtn": - { - var textBox = new TextBox() { - Margin = new Thickness(10, 0, 0, 0), - Text = Settings[attribute.Name] as string ?? string.Empty, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; - - var Btn = new System.Windows.Controls.Button() - { - Margin = new Thickness(10, 0, 0, 0), Content = "Browse" - }; - - Btn.Click += (_, _) => - { - using CommonDialog dialog = type switch + var textBox = new TextBox() { - "inputWithFolderBtn" => new FolderBrowserDialog(), - _ => new OpenFileDialog(), + Width = SettingPanelPathTextBoxWidth, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftMargin, + Text = Settings[attributes.Name] as string ?? string.Empty, + ToolTip = attributes.Description }; - if (dialog.ShowDialog() != DialogResult.OK) return; - var path = dialog switch + textBox.TextChanged += (_, _) => { - FolderBrowserDialog folderDialog => folderDialog.SelectedPath, - OpenFileDialog fileDialog => fileDialog.FileName, + Settings[attributes.Name] = textBox.Text; }; - textBox.Text = path; - Settings[attribute.Name] = path; - }; - var dockPanel = new DockPanel() { Margin = settingControlMargin }; + var Btn = new Button() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftMargin, + Content = API.GetTranslation("select") + }; - DockPanel.SetDock(Btn, Dock.Right); - dockPanel.Children.Add(Btn); - dockPanel.Children.Add(textBox); - contentControl = dockPanel; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + Btn.Click += (_, _) => + { + using System.Windows.Forms.CommonDialog dialog = type switch + { + "inputWithFolderBtn" => new System.Windows.Forms.FolderBrowserDialog(), + _ => new System.Windows.Forms.OpenFileDialog(), + }; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); + if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK) + { + return; + } - break; - } + var path = dialog switch + { + System.Windows.Forms.FolderBrowserDialog folderDialog => folderDialog.SelectedPath, + System.Windows.Forms.OpenFileDialog fileDialog => fileDialog.FileName, + _ => throw new System.NotImplementedException() + }; + + textBox.Text = path; + Settings[attributes.Name] = path; + }; + + var stackPanel = new StackPanel() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemTopBottomMargin, + Orientation = Orientation.Horizontal + }; + + // Create a stack panel to wrap the button and text box + stackPanel.Children.Add(textBox); + stackPanel.Children.Add(Btn); + + contentControl = stackPanel; + + break; + } case "textarea": - { - var textBox = new TextBox() { - Height = 120, - Margin = settingControlMargin, - VerticalAlignment = VerticalAlignment.Center, - TextWrapping = TextWrapping.WrapWithOverflow, - AcceptsReturn = true, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - Text = Settings[attribute.Name] as string ?? string.Empty, - ToolTip = attribute.Description - }; + var textBox = new TextBox() + { + MinHeight = SettingPanelAreaTextBoxMinHeight, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + TextWrapping = TextWrapping.WrapWithOverflow, + AcceptsReturn = true, + Text = Settings[attributes.Name] as string ?? string.Empty, + ToolTip = attributes.Description + }; - textBox.TextChanged += (sender, _) => - { - Settings[attribute.Name] = ((TextBox)sender).Text; - }; + textBox.TextChanged += (sender, _) => + { + Settings[attributes.Name] = ((TextBox)sender).Text; + }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = textBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "passwordBox": - { - var passwordBox = new PasswordBox() { - Margin = settingControlMargin, - Password = Settings[attribute.Name] as string ?? string.Empty, - PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; + var passwordBox = new PasswordBox() + { + MinWidth = SettingPanelTextBoxMinWidth, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + Password = Settings[attributes.Name] as string ?? string.Empty, + PasswordChar = attributes.passwordChar == default ? '*' : attributes.passwordChar, + ToolTip = attributes.Description, + }; - passwordBox.PasswordChanged += (sender, _) => - { - Settings[attribute.Name] = ((PasswordBox)sender).Password; - }; + passwordBox.PasswordChanged += (sender, _) => + { + Settings[attributes.Name] = ((PasswordBox)sender).Password; + }; - contentControl = passwordBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = passwordBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "dropdown": - { - var comboBox = new System.Windows.Controls.ComboBox() { - ItemsSource = attribute.Options, - SelectedItem = Settings[attribute.Name], - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; + var comboBox = new ComboBox() + { + ItemsSource = attributes.Options, + SelectedItem = Settings[attributes.Name], + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + ToolTip = attributes.Description + }; - comboBox.SelectionChanged += (sender, _) => - { - Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; - }; + comboBox.SelectionChanged += (sender, _) => + { + Settings[attributes.Name] = (string)((ComboBox)sender).SelectedItem; + }; - contentControl = comboBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = comboBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "checkbox": - var checkBox = new CheckBox { - IsChecked = - Settings[attribute.Name] is bool isChecked + // If can parse the default value to bool, use it, otherwise use false + var defaultValue = bool.TryParse(attributes.DefaultValue, out var value) && value; + var checkBox = new CheckBox + { + IsChecked = + Settings[attributes.Name] is bool isChecked ? isChecked - : bool.Parse(attribute.DefaultValue), - Margin = settingCheckboxMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; + : defaultValue, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemTopBottomMargin, + Content = attributes.Label, + ToolTip = attributes.Description + }; - checkBox.Click += (sender, _) => - { - Settings[attribute.Name] = ((CheckBox)sender).IsChecked; - }; + checkBox.Click += (sender, _) => + { + Settings[attributes.Name] = ((CheckBox)sender).IsChecked ?? defaultValue; + }; - contentControl = checkBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = checkBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; + break; + } case "hyperlink": - var hyperlink = new Hyperlink { ToolTip = attribute.Description, NavigateUri = attribute.url }; - - var linkbtn = new System.Windows.Controls.Button { - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - Margin = settingControlMargin - }; + var hyperlink = new Hyperlink + { + ToolTip = attributes.Description, + NavigateUri = attributes.url + }; - linkbtn.Content = attribute.urlLabel; + hyperlink.Inlines.Add(attributes.urlLabel); + hyperlink.RequestNavigate += (sender, e) => + { + API.OpenUrl(e.Uri); + e.Handled = true; + }; - contentControl = linkbtn; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + var textBlock = new TextBlock() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + TextAlignment = TextAlignment.Left, + TextWrapping = TextWrapping.Wrap + }; + textBlock.Inlines.Add(hyperlink); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); + contentControl = textBlock; - break; + break; + } + case "separator": + { + var sep = new Separator(); + + sep.SetResourceReference(Separator.StyleProperty, "SettingPanelSeparatorStyle"); + + contentControl = sep; + + break; + } default: continue; } - if (type != "textBlock") - SettingControls[attribute.Name] = contentControl; + // If type is textBlock or separator, we just add the content control to the main grid + if (panel == null) + { + // Add the content control to the column 0, row rowCount and columnSpan 2 of the main grid + mainPanel.Children.Add(contentControl); + Grid.SetColumn(contentControl, 0); + Grid.SetColumnSpan(contentControl, 2); + Grid.SetRow(contentControl, rowCount); + } + else + { + // Add the panel to the column 0 and row rowCount of the main grid + mainPanel.Children.Add(panel); + Grid.SetColumn(panel, 0); + Grid.SetRow(panel, rowCount); + + // Add the content control to the column 1 and row rowCount of the main grid + mainPanel.Children.Add(contentControl); + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + } + + // Add into SettingControls for settings storage if need + if (NeedSaveInSettings(type)) SettingControls[attributes.Name] = contentControl; - mainPanel.Children.Add(panel); - mainPanel.Children.Add(contentControl); rowCount++; } - return settingWindow; + // Wrap the main grid in a user control + return new UserControl() + { + Content = mainPanel + }; + } + + private static bool NeedSaveInSettings(string type) + { + return type != "textBlock" && type != "separator" && type != "hyperlink"; } } } diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 09711051e..b1396e207 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -231,7 +231,6 @@ namespace Flow.Launcher.Core.Plugin if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword)) return GlobalPlugins; - var plugin = NonGlobalPlugins[query.ActionKeyword]; return new List { @@ -367,7 +366,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; + } } /// @@ -388,16 +396,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 3de2bdb61..7200b23eb 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; @@ -457,4 +458,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 5300c2550..8b3ca6331 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -33,7 +33,9 @@ namespace Flow.Launcher.Plugin public string ActionKeyword { get; set; } public List ActionKeywords { get; set; } - + + public bool HideActionKeywordPanel { get; set; } + public int SearchDelay { get; set; } public string IcoPath { get; set;} diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index e182491c2..15b2dd171 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -39,10 +39,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/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index d4508ad67..d5567656c 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -94,10 +94,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Flow.Launcher/Helper/DWMDropShadow.cs b/Flow.Launcher/Helper/DWMDropShadow.cs deleted file mode 100644 index 58817d70e..000000000 --- a/Flow.Launcher/Helper/DWMDropShadow.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Interop; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.Graphics.Dwm; -using Windows.Win32.UI.Controls; - -namespace Flow.Launcher.Helper; - -public class DwmDropShadow -{ - - /// - /// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven). - /// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect, - /// as AllowsTransparency involves a huge permormance issue (hardware acceleration is turned off for all the window). - /// - /// Window to which the shadow will be applied - public static void DropShadowToWindow(Window window) - { - if (!DropShadow(window)) - { - window.SourceInitialized += window_SourceInitialized; - } - } - - private static void window_SourceInitialized(object sender, EventArgs e) //fixed typo - { - Window window = (Window)sender; - - DropShadow(window); - - window.SourceInitialized -= window_SourceInitialized; - } - - /// - /// The actual method that makes API calls to drop the shadow to the window - /// - /// Window to which the shadow will be applied - /// True if the method succeeded, false if not - private static unsafe bool DropShadow(Window window) - { - try - { - WindowInteropHelper helper = new WindowInteropHelper(window); - int val = 2; - var ret1 = PInvoke.DwmSetWindowAttribute(new (helper.Handle), DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY, &val, 4); - - if (ret1 == HRESULT.S_OK) - { - var m = new MARGINS { cyBottomHeight = 0, cxLeftWidth = 0, cxRightWidth = 0, cyTopHeight = 0 }; - var ret2 = PInvoke.DwmExtendFrameIntoClientArea(new(helper.Handle), &m); - return ret2 == HRESULT.S_OK; - } - - return false; - } - catch (Exception) - { - // Probably dwmapi.dll not found (incompatible OS) - return false; - } - } - -} diff --git a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs index a3bd83a97..151ce97dd 100644 --- a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs +++ b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs @@ -2,19 +2,16 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; +using Flow.Launcher.Infrastructure; using Microsoft.Win32; -using Windows.Win32; -using Windows.Win32.UI.WindowsAndMessaging; namespace Flow.Launcher.Helper; public static class WallpaperPathRetrieval { - private static readonly int MAX_PATH = 260; private static readonly int MAX_CACHE_SIZE = 3; private static readonly Dictionary<(string, DateTime), ImageBrush> wallpaperCache = new(); @@ -29,7 +26,7 @@ public static class WallpaperPathRetrieval try { - var wallpaperPath = GetWallpaperPath(); + var wallpaperPath = Win32Helper.GetWallpaperPath(); if (wallpaperPath is not null && File.Exists(wallpaperPath)) { // Since the wallpaper file name can be the same (TranscodedWallpaper), @@ -78,17 +75,6 @@ public static class WallpaperPathRetrieval } } - private static unsafe string GetWallpaperPath() - { - var wallpaperPtr = stackalloc char[MAX_PATH]; - PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, (uint)MAX_PATH, - wallpaperPtr, - 0); - var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr); - - return wallpaper.ToString(); - } - private static Color GetWallpaperColor() { RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", true); diff --git a/Flow.Launcher/Helper/WindowsInteropHelper.cs b/Flow.Launcher/Helper/WindowsInteropHelper.cs deleted file mode 100644 index 3e57948a5..000000000 --- a/Flow.Launcher/Helper/WindowsInteropHelper.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.ComponentModel; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Windows; -using System.Windows.Forms; -using System.Windows.Interop; -using System.Windows.Media; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.UI.WindowsAndMessaging; -using Point = System.Windows.Point; - -namespace Flow.Launcher.Helper; - -public class WindowsInteropHelper -{ - private static HWND _hwnd_shell; - private static HWND _hwnd_desktop; - - //Accessors for shell and desktop handlers - //Will set the variables once and then will return them - private static HWND HWND_SHELL - { - get - { - return _hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow(); - } - } - - private static HWND HWND_DESKTOP - { - get - { - return _hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow(); - } - } - - const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass"; - const string WINDOW_CLASS_WINTAB = "Flip3D"; - const string WINDOW_CLASS_PROGMAN = "Progman"; - const string WINDOW_CLASS_WORKERW = "WorkerW"; - - public unsafe static bool IsWindowFullscreen() - { - //get current active window - var hWnd = PInvoke.GetForegroundWindow(); - - if (hWnd.Equals(HWND.Null)) - { - 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; - } - } - - Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds; - return (appBounds.bottom - appBounds.top) == screenBounds.Height && - (appBounds.right - appBounds.left) == screenBounds.Width; - } - - /// - /// disable windows toolbar's control box - /// this will also disable system menu with Alt+Space hotkey - /// - public static void DisableControlBox(Window win) - { - var hwnd = new HWND(new WindowInteropHelper(win).Handle); - - var style = PInvoke.GetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE); - - if (style == 0) - { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } - - style &= ~(int)WINDOW_STYLE.WS_SYSMENU; - - var previousStyle = PInvoke.SetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, - style); - - if (previousStyle == 0) - { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } - } - - /// - /// Transforms pixels to Device Independent Pixels used by WPF - /// - /// 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) - { - Matrix matrix; - var source = PresentationSource.FromVisual(visual); - if (source is not null) - { - matrix = source.CompositionTarget.TransformFromDevice; - } - else - { - using var src = new HwndSource(new HwndSourceParameters()); - matrix = src.CompositionTarget.TransformFromDevice; - } - - return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY)); - } - - #region Alt Tab - - private static int SetWindowLong(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong) - { - PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error - - var result = PInvoke.SetWindowLong(hWnd, nIndex, dwNewLong); - if (result == 0 && Marshal.GetLastPInvokeError() != 0) - { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } - - return result; - } - - /// - /// Hide windows in the Alt+Tab window list - /// - /// To hide a window - public static void HideFromAltTab(Window window) - { - var exStyle = GetCurrentWindowStyle(window); - - // Add TOOLWINDOW style, remove APPWINDOW style - var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW; - - SetWindowLong(new(new WindowInteropHelper(window).Handle), 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 exStyle = GetCurrentWindowStyle(window); - - // 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; - - SetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle); - } - - /// - /// To obtain the current overridden style of a window. - /// - /// To obtain the style dialog window - /// current extension style value - private static int GetCurrentWindowStyle(Window window) - { - var style = PInvoke.GetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); - if (style == 0 && Marshal.GetLastPInvokeError() != 0) - { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } - return style; - } - - #endregion -} diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index c47e86ff1..678280bba 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -214,13 +214,14 @@ namespace Flow.Launcher HotkeyList.ItemsSource = KeysToDisplay; - RefreshHotkeyInterface(Hotkey); + // We should not call RefreshHotkeyInterface here because DependencyProperty is not set yet + // And it will be called in OnHotkeyChanged event or Hotkey setter later } private void RefreshHotkeyInterface(string hotkey) { - SetKeysToDisplay(new HotkeyModel(Hotkey)); - CurrentHotkey = new HotkeyModel(Hotkey); + SetKeysToDisplay(new HotkeyModel(hotkey)); + CurrentHotkey = new HotkeyModel(hotkey); } private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 83f0df08d..7eac8a779 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -108,6 +108,11 @@ Always Preview Always open preview panel when Flow activates. Press {0} to toggle preview. Shadow effect is not allowed while current theme has blur effect enabled + Backdrop Type + None + Acrylic + Mica + Mica Alt Search Plugin @@ -342,9 +347,10 @@ Can't find specified plugin New Action Keyword can't be empty This new Action Keyword is already assigned to another plugin, please choose a different one + This new Action Keyword is the same as old, please choose a different one Success Completed successfully - Enter the action keyword you like to use to start the plugin. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords. + Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords. Custom Query Hotkey diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index df681470a..bac953128 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -20,17 +20,17 @@ Closing="OnClosing" Deactivated="OnDeactivated" Icon="Images/app.png" - SourceInitialized="OnSourceInitialized" - Initialized="OnInitialized" Left="{Binding Settings.WindowLeft, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Loaded="OnLoaded" LocationChanged="OnLocationChanged" Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" PreviewKeyDown="OnKeyDown" PreviewKeyUp="OnKeyUp" + PreviewMouseMove="OnPreviewMouseMove" ResizeMode="CanResize" ShowInTaskbar="False" SizeToContent="Height" + SourceInitialized="OnSourceInitialized" Topmost="True" Visibility="{Binding MainWindowVisibility, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" WindowStartupLocation="Manual" @@ -240,14 +240,14 @@ FontSize="{Binding QueryBoxFontSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" InputMethod.PreferredImeConversionMode="{Binding StartWithEnglishMode, Converter={StaticResource BoolToIMEConversionModeConverter}}" InputMethod.PreferredImeState="{Binding StartWithEnglishMode, Converter={StaticResource BoolToIMEStateConverter}}" - PreviewDragOver="OnPreviewDragOver" + PreviewDragOver="QueryTextBox_OnPreviewDragOver" PreviewKeyUp="QueryTextBox_KeyUp" Style="{DynamicResource QueryBoxStyle}" Text="{Binding QueryText, Mode=OneWay}" Visibility="Visible" WindowChrome.IsHitTestVisibleInChrome="True"> - + diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index a615bfa2f..82e7046cf 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,24 +1,26 @@ using System; using System.ComponentModel; +using System.Linq; +using System.Media; using System.Threading.Tasks; using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Controls; +using System.Windows.Shapes; +using System.Windows.Threading; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; -using Flow.Launcher.Helper; -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.ViewModel; -using Screen = System.Windows.Forms.Screen; -using DragEventArgs = System.Windows.DragEventArgs; -using KeyEventArgs = System.Windows.Input.KeyEventArgs; -using NotifyIcon = System.Windows.Forms.NotifyIcon; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.SharedCommands; -using System.Windows.Threading; -using System.Windows.Data; +using Flow.Launcher.ViewModel; using ModernWpf.Controls; using Key = System.Windows.Input.Key; using System.Media; @@ -28,6 +30,9 @@ using System.Windows.Interop; using Windows.Win32; using System.Reactive.Linq; using System.Windows.Shapes; +using MouseButtons = System.Windows.Forms.MouseButtons; +using NotifyIcon = System.Windows.Forms.NotifyIcon; +using Screen = System.Windows.Forms.Screen; namespace Flow.Launcher { @@ -35,162 +40,114 @@ namespace Flow.Launcher { #region Private Fields - private Settings _settings; + // Dependency Injection + private readonly Settings _settings; + private readonly Theme _theme; + + // Window Notify Icon private NotifyIcon _notifyIcon; - private ContextMenu contextMenu = new ContextMenu(); - private MainViewModel _viewModel; - private bool _animating; + + // Window Context Menu + private readonly ContextMenu contextMenu = new(); + private readonly MainViewModel _viewModel; + + // Window Event : Key Event private bool isArrowKeyPressed = false; + // Window Sound Effects private MediaPlayer animationSoundWMP; private SoundPlayer animationSoundWPF; - #endregion - - public MainWindow(Settings settings, MainViewModel mainVM) - { - DataContext = mainVM; - _viewModel = mainVM; - _settings = settings; - - InitializeComponent(); - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePosition(); - InitializePosition(); - - InitSoundEffects(); - - DataObject.AddPastingHandler(QueryTextBox, OnPaste); - - Loaded += (_, _) => - { - var handle = new WindowInteropHelper(this).Handle; - var win = HwndSource.FromHwnd(handle); - win.AddHook(WndProc); - }; - } - + // Window WndProc private int _initialWidth; private int _initialHeight; - private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + // Window Animation + private const double DefaultRightMargin = 66; //* this value from base.xaml + private bool _animating; + private bool _isClockPanelAnimating = false; // 애니메이션 실행 중인지 여부 + + #endregion + + #region Constructor + + public MainWindow() { - if (msg == PInvoke.WM_ENTERSIZEMOVE) - { - _initialWidth = (int)Width; - _initialHeight = (int)Height; - handled = true; - } + _settings = Ioc.Default.GetRequiredService(); + _theme = Ioc.Default.GetRequiredService(); + _viewModel = Ioc.Default.GetRequiredService(); + DataContext = _viewModel; - if (msg == PInvoke.WM_EXITSIZEMOVE) - { - if (_initialHeight != (int)Height) - { - OnResizeEnd(); - } + InitializeComponent(); + UpdatePosition(true); - if (_initialWidth != (int)Width) - { - FlowMainWindow.SizeToContent = SizeToContent.Height; - } - - handled = true; - } - - return IntPtr.Zero; + InitSoundEffects(); + DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste); } - private void OnResizeEnd() - { - int shadowMargin = 0; - if (_settings.UseDropShadowEffect) - { - shadowMargin = 32; - } + #endregion - if (!_settings.KeepMaxResults) - { - var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize; + #region Window Event - if (itemCount < 2) - { - _settings.MaxResultsToShow = 2; - } - else - { - _settings.MaxResultsToShow = Convert.ToInt32(Math.Truncate(itemCount)); - } - } - - FlowMainWindow.SizeToContent = SizeToContent.Height; - _viewModel.MainWindowWidth = Width; - } - - private void OnCopy(object sender, ExecutedRoutedEventArgs e) - { - var result = _viewModel.Results.SelectedItem?.Result; - if (QueryTextBox.SelectionLength == 0 && result != null) - { - string copyText = result.CopyText; - App.API.CopyToClipboard(copyText, directCopy: true); - } - else if (!string.IsNullOrEmpty(QueryTextBox.Text)) - { - App.API.CopyToClipboard(QueryTextBox.SelectedText, showDefaultNotification: false); - } - } - - private void OnPaste(object sender, DataObjectPastingEventArgs e) - { - var isText = e.SourceDataObject.GetDataPresent(System.Windows.DataFormats.UnicodeText, true); - if (isText) - { - var text = e.SourceDataObject.GetData(System.Windows.DataFormats.UnicodeText) as string; - text = text.Replace(Environment.NewLine, " "); - DataObject data = new DataObject(); - data.SetData(System.Windows.DataFormats.UnicodeText, text); - e.DataObject = data; - } - } - - private async void OnClosing(object sender, CancelEventArgs e) - { - _notifyIcon.Visible = false; - App.API.SaveAppAllSettings(); - e.Cancel = true; - await PluginManager.DisposePluginsAsync(); - Notification.Uninstall(); - Environment.Exit(0); - } +#pragma warning disable VSTHRD100 // Avoid async void methods private void OnSourceInitialized(object sender, EventArgs e) { - WindowsInteropHelper.HideFromAltTab(this); + var handle = Win32Helper.GetWindowHandle(this, true); + var win = HwndSource.FromHwnd(handle); + win.AddHook(WndProc); + Win32Helper.HideFromAltTab(this); + Win32Helper.DisableControlBox(this); } - private void OnInitialized(object sender, EventArgs e) + private async void OnLoaded(object sender, RoutedEventArgs _) { - } + // Check first launch + if (_settings.FirstLaunch) + { + _settings.FirstLaunch = false; + App.API.SaveAppAllSettings(); + var WelcomeWindow = new WelcomeWindow(); + WelcomeWindow.Show(); + } - private void OnLoaded(object sender, RoutedEventArgs _) - { - // MouseEventHandler - PreviewMouseMove += MainPreviewMouseMove; - CheckFirstLaunch(); - HideStartup(); - // show notify icon when flowlauncher is hidden + // Hide window if need + UpdatePosition(true); + if (_settings.HideOnStartup) + { + _viewModel.Hide(); + } + else + { + _viewModel.Show(); + } + + // Show notify icon when flowlauncher is hidden InitializeNotifyIcon(); - InitializeColorScheme(); - WindowsInteropHelper.DisableControlBox(this); + + // Initialize color scheme + if (_settings.ColorScheme == Constant.Light) + { + ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light; + } + else if (_settings.ColorScheme == Constant.Dark) + { + ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark; + } + + // Initialize position InitProgressbarAnimation(); - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePosition(); - InitializePosition(); - PreviewReset(); - // Setup search text box reactiveness - SetupSearchTextBoxReactiveness(_settings.SearchQueryResultsWithDelay); - // since the default main window visibility is visible - // so we need set focus during startup + + // Force update position + UpdatePosition(true); + + // Refresh frame + await Ioc.Default.GetRequiredService().RefreshFrameAsync(); + + // Reset preview + _viewModel.ResetPreview(); + + // Since the default main window visibility is visible, so we need set focus during startup QueryTextBox.Focus(); _viewModel.PropertyChanged += (o, e) => @@ -207,9 +164,9 @@ namespace Flow.Launcher { SoundPlay(); } - - UpdatePosition(); - PreviewReset(); + + UpdatePosition(false); + _viewModel.ResetPreview(); Activate(); QueryTextBox.Focus(); _settings.ActivateTimes++; @@ -220,7 +177,7 @@ namespace Flow.Launcher } if (_settings.UseAnimation) - WindowAnimator(); + WindowAnimation(); } }); break; @@ -228,7 +185,9 @@ namespace Flow.Launcher case nameof(MainViewModel.QueryTextCursorMovedToEnd): if (_viewModel.QueryTextCursorMovedToEnd) { - MoveQueryTextToEnd(); + // QueryTextBox seems to be update with a DispatcherPriority as low as ContextIdle. + // To ensure QueryTextBox is up to date with QueryText from the View, we need to Dispatch with such a priority + Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length); _viewModel.QueryTextCursorMovedToEnd = false; } @@ -265,395 +224,29 @@ namespace Flow.Launcher break; } }; + + // ✅ QueryTextBox.Text 변경 감지 (글자 수 1 이상일 때만 동작하도록 수정) + QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility(); + + // ✅ ContextMenu.Visibility 변경 감지 + DependencyPropertyDescriptor + .FromProperty(VisibilityProperty, typeof(ContextMenu)) + .AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility()); + + // ✅ History.Visibility 변경 감지 + DependencyPropertyDescriptor + .FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정 + .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } - private void InitializePosition() + private async void OnClosing(object sender, CancelEventArgs e) { - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePositionInner(); - InitializePositionInner(); - return; - - void InitializePositionInner() - { - if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) - { - Top = _settings.WindowTop; - Left = _settings.WindowLeft; - } - else - { - var screen = SelectedScreen(); - switch (_settings.SearchWindowAlign) - { - case SearchWindowAligns.Center: - Left = HorizonCenter(screen); - Top = VerticalCenter(screen); - break; - case SearchWindowAligns.CenterTop: - Left = HorizonCenter(screen); - Top = 10; - break; - case SearchWindowAligns.LeftTop: - Left = HorizonLeft(screen); - Top = 10; - break; - case SearchWindowAligns.RightTop: - Left = HorizonRight(screen); - Top = 10; - break; - case SearchWindowAligns.Custom: - Left = WindowsInteropHelper.TransformPixelsToDIP(this, - screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; - Top = WindowsInteropHelper.TransformPixelsToDIP(this, 0, - screen.WorkingArea.Y + _settings.CustomWindowTop).Y; - break; - } - } - } - } - - private void UpdateNotifyIconText() - { - var menu = contextMenu; - ((MenuItem)menu.Items[0]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + - " (" + _settings.Hotkey + ")"; - ((MenuItem)menu.Items[1]).Header = InternationalizationManager.Instance.GetTranslation("GameMode"); - ((MenuItem)menu.Items[2]).Header = InternationalizationManager.Instance.GetTranslation("PositionReset"); - ((MenuItem)menu.Items[3]).Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings"); - ((MenuItem)menu.Items[4]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit"); - } - - private void InitializeNotifyIcon() - { - _notifyIcon = new NotifyIcon - { - Text = Infrastructure.Constant.FlowLauncherFullName, - Icon = Constant.Version == "1.0.0" ? Properties.Resources.dev : Properties.Resources.app, - Visible = !_settings.HideNotifyIcon - }; - var openIcon = new FontIcon { Glyph = "\ue71e" }; - var open = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + - _settings.Hotkey + ")", - Icon = openIcon - }; - var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" }; - var gamemode = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("GameMode"), - Icon = gamemodeIcon - }; - var positionresetIcon = new FontIcon { Glyph = "\ue73f" }; - var positionreset = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("PositionReset"), - Icon = positionresetIcon - }; - var settingsIcon = new FontIcon { Glyph = "\ue713" }; - var settings = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings"), - Icon = settingsIcon - }; - var exitIcon = new FontIcon { Glyph = "\ue7e8" }; - var exit = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit"), - Icon = exitIcon - }; - - open.Click += (o, e) => _viewModel.ToggleFlowLauncher(); - gamemode.Click += (o, e) => _viewModel.ToggleGameMode(); - positionreset.Click += (o, e) => PositionReset(); - settings.Click += (o, e) => App.API.OpenSettingDialog(); - exit.Click += (o, e) => Close(); - - gamemode.ToolTip = InternationalizationManager.Instance.GetTranslation("GameModeToolTip"); - positionreset.ToolTip = InternationalizationManager.Instance.GetTranslation("PositionResetToolTip"); - - contextMenu.Items.Add(open); - contextMenu.Items.Add(gamemode); - contextMenu.Items.Add(positionreset); - contextMenu.Items.Add(settings); - contextMenu.Items.Add(exit); - - _notifyIcon.MouseClick += (o, e) => - { - switch (e.Button) - { - case System.Windows.Forms.MouseButtons.Left: - _viewModel.ToggleFlowLauncher(); - break; - case System.Windows.Forms.MouseButtons.Right: - - contextMenu.IsOpen = true; - // Get context menu handle and bring it to the foreground - if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource) - { - PInvoke.SetForegroundWindow(new(hwndSource.Handle)); - } - - contextMenu.Focus(); - break; - } - }; - } - - private void CheckFirstLaunch() - { - if (_settings.FirstLaunch) - { - _settings.FirstLaunch = false; - App.API.SaveAppAllSettings(); - OpenWelcomeWindow(); - } - } - - private void OpenWelcomeWindow() - { - var WelcomeWindow = new WelcomeWindow(); - WelcomeWindow.Show(); - } - - private async void PositionReset() - { - _viewModel.Show(); - await Task.Delay(300); // If don't give a time, Positioning will be weird. - var screen = SelectedScreen(); - Left = HorizonCenter(screen); - Top = VerticalCenter(screen); - } - - private void InitProgressbarAnimation() - { - var progressBarStoryBoard = new Storyboard(); - - var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, - new Duration(new TimeSpan(0, 0, 0, 0, 1600))); - var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth + 0, - new Duration(new TimeSpan(0, 0, 0, 0, 1600))); - Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)")); - Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)")); - progressBarStoryBoard.Children.Add(da); - progressBarStoryBoard.Children.Add(da1); - progressBarStoryBoard.RepeatBehavior = RepeatBehavior.Forever; - - da.Freeze(); - da1.Freeze(); - - const string progressBarAnimationName = "ProgressBarAnimation"; - var beginStoryboard = new BeginStoryboard - { - Name = progressBarAnimationName, Storyboard = progressBarStoryBoard - }; - - var stopStoryboard = new StopStoryboard() - { - BeginStoryboardName = progressBarAnimationName - }; - - var trigger = new Trigger - { - Property = VisibilityProperty, Value = Visibility.Visible - }; - trigger.EnterActions.Add(beginStoryboard); - trigger.ExitActions.Add(stopStoryboard); - - var progressStyle = new Style(typeof(Line)) - { - BasedOn = FindResource("PendingLineStyle") as Style - }; - progressStyle.RegisterName(progressBarAnimationName, beginStoryboard); - progressStyle.Triggers.Add(trigger); - - ProgressBar.Style = progressStyle; - - _viewModel.ProgressBarVisibility = Visibility.Hidden; - } - - public void WindowAnimator() - { - if (_animating) - return; - - isArrowKeyPressed = true; - _animating = true; - UpdatePosition(); - - Storyboard windowsb = new Storyboard(); - Storyboard clocksb = new Storyboard(); - Storyboard iconsb = new Storyboard(); - CircleEase easing = new CircleEase(); - easing.EasingMode = EasingMode.EaseInOut; - - var animationLength = _settings.AnimationSpeed switch - { - AnimationSpeeds.Slow => 560, - AnimationSpeeds.Medium => 360, - AnimationSpeeds.Fast => 160, - _ => _settings.CustomAnimationLength - }; - - var WindowOpacity = new DoubleAnimation - { - From = 0, - To = 1, - Duration = TimeSpan.FromMilliseconds(animationLength * 2 / 3), - FillBehavior = FillBehavior.Stop - }; - - var WindowMotion = new DoubleAnimation - { - From = Top + 10, - To = Top, - Duration = TimeSpan.FromMilliseconds(animationLength * 2 / 3), - FillBehavior = FillBehavior.Stop - }; - var IconMotion = new DoubleAnimation - { - From = 12, - To = 0, - EasingFunction = easing, - Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop - }; - - var ClockOpacity = new DoubleAnimation - { - From = 0, - To = 1, - EasingFunction = easing, - Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop - }; - double TargetIconOpacity = SearchIcon.Opacity; // Animation Target Opacity from Style - var IconOpacity = new DoubleAnimation - { - From = 0, - To = TargetIconOpacity, - EasingFunction = easing, - Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop - }; - - double right = ClockPanel.Margin.Right; - var thicknessAnimation = new ThicknessAnimation - { - From = new Thickness(0, 12, right, 0), - To = new Thickness(0, 0, right, 0), - EasingFunction = easing, - Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop - }; - - Storyboard.SetTargetProperty(ClockOpacity, new PropertyPath(OpacityProperty)); - Storyboard.SetTargetName(thicknessAnimation, "ClockPanel"); - Storyboard.SetTargetProperty(thicknessAnimation, new PropertyPath(MarginProperty)); - Storyboard.SetTarget(WindowOpacity, this); - Storyboard.SetTargetProperty(WindowOpacity, new PropertyPath(Window.OpacityProperty)); - Storyboard.SetTargetProperty(WindowMotion, new PropertyPath(Window.TopProperty)); - Storyboard.SetTargetProperty(IconMotion, new PropertyPath(TopProperty)); - Storyboard.SetTargetProperty(IconOpacity, new PropertyPath(OpacityProperty)); - - clocksb.Children.Add(thicknessAnimation); - clocksb.Children.Add(ClockOpacity); - windowsb.Children.Add(WindowOpacity); - windowsb.Children.Add(WindowMotion); - iconsb.Children.Add(IconMotion); - iconsb.Children.Add(IconOpacity); - - windowsb.Completed += (_, _) => _animating = false; - _settings.WindowLeft = Left; - _settings.WindowTop = Top; - isArrowKeyPressed = false; - - if (QueryTextBox.Text.Length == 0) - { - clocksb.Begin(ClockPanel); - } - - iconsb.Begin(SearchIcon); - windowsb.Begin(FlowMainWindow); - } - - private void InitSoundEffects() - { - if (_settings.WMPInstalled) - { - animationSoundWMP = new MediaPlayer(); - animationSoundWMP.Open(new Uri(AppDomain.CurrentDomain.BaseDirectory + "Resources\\open.wav")); - } - else - { - animationSoundWPF = new SoundPlayer(AppDomain.CurrentDomain.BaseDirectory + "Resources\\open.wav"); - } - } - - private void SoundPlay() - { - if (_settings.WMPInstalled) - { - animationSoundWMP.Position = TimeSpan.Zero; - animationSoundWMP.Volume = _settings.SoundVolume / 100.0; - animationSoundWMP.Play(); - } - else - { - animationSoundWPF.Play(); - } - } - - private void OnMouseDown(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left) DragMove(); - } - - private void OnPreviewDragOver(object sender, DragEventArgs e) - { - e.Handled = true; - } - - private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs e) - { - _viewModel.Hide(); - - if (_settings.UseAnimation) - await Task.Delay(100); - - App.API.OpenSettingDialog(); - } - - private async void OnDeactivated(object sender, EventArgs e) - { - _settings.WindowLeft = Left; - _settings.WindowTop = Top; - //This condition stops extra hide call when animator is on, - // which causes the toggling to occasional hide instead of show. - if (_viewModel.MainWindowVisibilityStatus) - { - // Need time to initialize the main query window animation. - // This also stops the mainwindow from flickering occasionally after Settings window is opened - // and always after Settings window is closed. - if (_settings.UseAnimation) - await Task.Delay(100); - - if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible) - { - _viewModel.Hide(); - } - } - } - - private void UpdatePosition() - { - if (_animating) - return; - - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePosition(); - InitializePosition(); + _notifyIcon.Visible = false; + App.API.SaveAppAllSettings(); + e.Cancel = true; + await PluginManager.DisposePluginsAsync(); + Notification.Uninstall(); + Environment.Exit(0); } private void OnLocationChanged(object sender, EventArgs e) @@ -667,83 +260,30 @@ namespace Flow.Launcher } } - public void HideStartup() + private async void OnDeactivated(object sender, EventArgs e) { - UpdatePosition(); - if (_settings.HideOnStartup) + _settings.WindowLeft = Left; + _settings.WindowTop = Top; + ClockPanel.Opacity = 0; + SearchIcon.Opacity = 0; + //This condition stops extra hide call when animator is on, + // which causes the toggling to occasional hide instead of show. + if (_viewModel.MainWindowVisibilityStatus) { - _viewModel.Hide(); - } - else - { - _viewModel.Show(); + // Need time to initialize the main query window animation. + // This also stops the mainwindow from flickering occasionally after Settings window is opened + // and always after Settings window is closed. + if (_settings.UseAnimation) + + await Task.Delay(100); + + if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible) + { + _viewModel.Hide(); + } } } - public Screen SelectedScreen() - { - Screen screen = null; - switch (_settings.SearchWindowScreen) - { - case SearchWindowScreens.Cursor: - screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - break; - case SearchWindowScreens.Primary: - screen = Screen.PrimaryScreen; - break; - case SearchWindowScreens.Focus: - var foregroundWindowHandle = PInvoke.GetForegroundWindow().Value; - screen = Screen.FromHandle(foregroundWindowHandle); - break; - case SearchWindowScreens.Custom: - if (_settings.CustomScreenNumber <= Screen.AllScreens.Length) - screen = Screen.AllScreens[_settings.CustomScreenNumber - 1]; - else - screen = Screen.AllScreens[0]; - break; - default: - screen = Screen.AllScreens[0]; - break; - } - - return screen ?? Screen.AllScreens[0]; - } - - public double HorizonCenter(Screen screen) - { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); - var left = (dip2.X - ActualWidth) / 2 + dip1.X; - return left; - } - - public double VerticalCenter(Screen screen) - { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); - var top = (dip2.Y - QueryTextBox.ActualHeight) / 4 + dip1.Y; - return top; - } - - public double HorizonRight(Screen screen) - { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); - var left = (dip1.X + dip2.X - ActualWidth) - 10; - return left; - } - - public double HorizonLeft(Screen screen) - { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var left = dip1.X + 10; - return left; - } - - /// - /// Register up and down key - /// todo: any way to put this in xaml ? - /// private void OnKeyDown(object sender, KeyEventArgs e) { var specialKeyState = GlobalHotkey.CheckModifiers(); @@ -817,7 +357,7 @@ namespace Flow.Launcher } } - private void MainPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) + private void OnPreviewMouseMove(object sender, MouseEventArgs e) { if (isArrowKeyPressed) { @@ -825,27 +365,625 @@ namespace Flow.Launcher } } - public void PreviewReset() +#pragma warning restore VSTHRD100 // Avoid async void methods + + #endregion + + #region Window Boarder Event + + private void OnMouseDown(object sender, MouseButtonEventArgs e) { - _viewModel.ResetPreview(); + if (e.ChangedButton == MouseButton.Left) DragMove(); } - private void MoveQueryTextToEnd() + #endregion + + #region Window Context Menu Event + +#pragma warning disable VSTHRD100 // Avoid async void methods + + private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs e) { - // QueryTextBox seems to be update with a DispatcherPriority as low as ContextIdle. - // To ensure QueryTextBox is up to date with QueryText from the View, we need to Dispatch with such a priority - Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length); + _viewModel.Hide(); + + if (_settings.UseAnimation) + await Task.Delay(100); + + App.API.OpenSettingDialog(); } - public void InitializeColorScheme() +#pragma warning restore VSTHRD100 // Avoid async void methods + + #endregion + + #region Window WndProc + + private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { - if (_settings.ColorScheme == Constant.Light) + if (msg == Win32Helper.WM_ENTERSIZEMOVE) { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light; + _initialWidth = (int)Width; + _initialHeight = (int)Height; + handled = true; } - else if (_settings.ColorScheme == Constant.Dark) + else if (msg == Win32Helper.WM_EXITSIZEMOVE) { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark; + if (_initialHeight != (int)Height) + { + var shadowMargin = 0; + var (_, useDropShadowEffect) = _theme.GetActualValue(); + if (useDropShadowEffect) + { + shadowMargin = 32; + } + + if (!_settings.KeepMaxResults) + { + var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize; + + if (itemCount < 2) + { + _settings.MaxResultsToShow = 2; + } + else + { + _settings.MaxResultsToShow = Convert.ToInt32(Math.Truncate(itemCount)); + } + } + + SizeToContent = SizeToContent.Height; + _viewModel.MainWindowWidth = Width; + } + + if (_initialWidth != (int)Width) + { + SizeToContent = SizeToContent.Height; + } + + handled = true; + } + + return IntPtr.Zero; + } + + #endregion + + #region Window Sound Effects + + private void InitSoundEffects() + { + if (_settings.WMPInstalled) + { + animationSoundWMP = new MediaPlayer(); + animationSoundWMP.Open(new Uri(AppContext.BaseDirectory + "Resources\\open.wav")); + } + else + { + animationSoundWPF = new SoundPlayer(AppContext.BaseDirectory + "Resources\\open.wav"); + } + } + + private void SoundPlay() + { + if (_settings.WMPInstalled) + { + animationSoundWMP.Position = TimeSpan.Zero; + animationSoundWMP.Volume = _settings.SoundVolume / 100.0; + animationSoundWMP.Play(); + } + else + { + animationSoundWPF.Play(); + } + } + + #endregion + + #region Window Notify Icon + + private void InitializeNotifyIcon() + { + _notifyIcon = new NotifyIcon + { + Text = Constant.FlowLauncherFullName, + Icon = Constant.Version == "1.0.0" ? Properties.Resources.dev : Properties.Resources.app, + Visible = !_settings.HideNotifyIcon + }; + var openIcon = new FontIcon { Glyph = "\ue71e" }; + var open = new MenuItem + { + Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")", + Icon = openIcon + }; + var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" }; + var gamemode = new MenuItem + { + Header = App.API.GetTranslation("GameMode"), + Icon = gamemodeIcon + }; + var positionresetIcon = new FontIcon { Glyph = "\ue73f" }; + var positionreset = new MenuItem + { + Header = App.API.GetTranslation("PositionReset"), + Icon = positionresetIcon + }; + var settingsIcon = new FontIcon { Glyph = "\ue713" }; + var settings = new MenuItem + { + Header = App.API.GetTranslation("iconTraySettings"), + Icon = settingsIcon + }; + var exitIcon = new FontIcon { Glyph = "\ue7e8" }; + var exit = new MenuItem + { + Header = App.API.GetTranslation("iconTrayExit"), + Icon = exitIcon + }; + + open.Click += (o, e) => _viewModel.ToggleFlowLauncher(); + gamemode.Click += (o, e) => _viewModel.ToggleGameMode(); + positionreset.Click += (o, e) => _ = PositionResetAsync(); + settings.Click += (o, e) => App.API.OpenSettingDialog(); + exit.Click += (o, e) => Close(); + + gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); + positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); + + contextMenu.Items.Add(open); + contextMenu.Items.Add(gamemode); + contextMenu.Items.Add(positionreset); + contextMenu.Items.Add(settings); + contextMenu.Items.Add(exit); + + _notifyIcon.MouseClick += (o, e) => + { + switch (e.Button) + { + case System.Windows.Forms.MouseButtons.Left: + _viewModel.ToggleFlowLauncher(); + break; + case System.Windows.Forms.MouseButtons.Right: + + contextMenu.IsOpen = true; + // Get context menu handle and bring it to the foreground + if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource) + { + Win32Helper.SetForegroundWindow(hwndSource.Handle); + } + + contextMenu.Focus(); + break; + } + }; + } + + private void UpdateNotifyIconText() + { + var menu = contextMenu; + ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") + + " (" + _settings.Hotkey + ")"; + ((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode"); + ((MenuItem)menu.Items[2]).Header = App.API.GetTranslation("PositionReset"); + ((MenuItem)menu.Items[3]).Header = App.API.GetTranslation("iconTraySettings"); + ((MenuItem)menu.Items[4]).Header = App.API.GetTranslation("iconTrayExit"); + } + + #endregion + + #region Window Position + + private void UpdatePosition(bool force) + { + if (_animating && !force) + { + return; + } + + // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + InitializePosition(); + InitializePosition(); + } + + private async Task PositionResetAsync() + { + _viewModel.Show(); + await Task.Delay(300); // If don't give a time, Positioning will be weird. + var screen = SelectedScreen(); + Left = HorizonCenter(screen); + Top = VerticalCenter(screen); + } + + private void InitializePosition() + { + // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + InitializePositionInner(); + InitializePositionInner(); + return; + + void InitializePositionInner() + { + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) + { + Top = _settings.WindowTop; + Left = _settings.WindowLeft; + } + else + { + var screen = SelectedScreen(); + switch (_settings.SearchWindowAlign) + { + case SearchWindowAligns.Center: + Left = HorizonCenter(screen); + Top = VerticalCenter(screen); + break; + case SearchWindowAligns.CenterTop: + Left = HorizonCenter(screen); + Top = 10; + break; + case SearchWindowAligns.LeftTop: + Left = HorizonLeft(screen); + Top = 10; + break; + case SearchWindowAligns.RightTop: + Left = HorizonRight(screen); + Top = 10; + break; + case SearchWindowAligns.Custom: + Left = Win32Helper.TransformPixelsToDIP(this, + screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; + Top = Win32Helper.TransformPixelsToDIP(this, 0, + screen.WorkingArea.Y + _settings.CustomWindowTop).Y; + break; + } + } + } + } + + private Screen SelectedScreen() + { + Screen screen; + switch (_settings.SearchWindowScreen) + { + case SearchWindowScreens.Cursor: + screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); + break; + case SearchWindowScreens.Primary: + screen = Screen.PrimaryScreen; + break; + case SearchWindowScreens.Focus: + var foregroundWindowHandle = Win32Helper.GetForegroundWindow(); + screen = Screen.FromHandle(foregroundWindowHandle); + break; + case SearchWindowScreens.Custom: + if (_settings.CustomScreenNumber <= Screen.AllScreens.Length) + screen = Screen.AllScreens[_settings.CustomScreenNumber - 1]; + else + screen = Screen.AllScreens[0]; + break; + default: + screen = Screen.AllScreens[0]; + break; + } + + return screen ?? Screen.AllScreens[0]; + } + + private double HorizonCenter(Screen screen) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var left = (dip2.X - ActualWidth) / 2 + dip1.X; + return left; + } + + private double VerticalCenter(Screen screen) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); + var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); + var top = (dip2.Y - QueryTextBox.ActualHeight) / 4 + dip1.Y; + return top; + } + + private double HorizonRight(Screen screen) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var left = (dip1.X + dip2.X - ActualWidth) - 10; + return left; + } + + private double HorizonLeft(Screen screen) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var left = dip1.X + 10; + return left; + } + + #endregion + + #region Window Animation + + private void InitProgressbarAnimation() + { + var progressBarStoryBoard = new Storyboard(); + + var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, + new Duration(new TimeSpan(0, 0, 0, 0, 1600))); + var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth + 0, + new Duration(new TimeSpan(0, 0, 0, 0, 1600))); + Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)")); + Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)")); + progressBarStoryBoard.Children.Add(da); + progressBarStoryBoard.Children.Add(da1); + progressBarStoryBoard.RepeatBehavior = RepeatBehavior.Forever; + + da.Freeze(); + da1.Freeze(); + + const string progressBarAnimationName = "ProgressBarAnimation"; + var beginStoryboard = new BeginStoryboard + { + Name = progressBarAnimationName, Storyboard = progressBarStoryBoard + }; + + var stopStoryboard = new StopStoryboard() + { + BeginStoryboardName = progressBarAnimationName + }; + + var trigger = new Trigger + { + Property = VisibilityProperty, Value = Visibility.Visible + }; + trigger.EnterActions.Add(beginStoryboard); + trigger.ExitActions.Add(stopStoryboard); + + var progressStyle = new Style(typeof(Line)) + { + BasedOn = FindResource("PendingLineStyle") as Style + }; + progressStyle.RegisterName(progressBarAnimationName, beginStoryboard); + progressStyle.Triggers.Add(trigger); + + ProgressBar.Style = progressStyle; + + _viewModel.ProgressBarVisibility = Visibility.Hidden; + } + + private void WindowAnimation() + { + if (_animating) + return; + + isArrowKeyPressed = true; + _animating = true; + UpdatePosition(false); + + ClockPanel.Opacity = 0; + SearchIcon.Opacity = 0; + + var clocksb = new Storyboard(); + var iconsb = new Storyboard(); + CircleEase easing = new CircleEase { EasingMode = EasingMode.EaseInOut }; + + var animationLength = _settings.AnimationSpeed switch + { + AnimationSpeeds.Slow => 560, + AnimationSpeeds.Medium => 360, + AnimationSpeeds.Fast => 160, + _ => _settings.CustomAnimationLength + }; + + var IconMotion = new DoubleAnimation + { + From = 12, + To = 0, + EasingFunction = easing, + Duration = TimeSpan.FromMilliseconds(animationLength), + FillBehavior = FillBehavior.HoldEnd + }; + + var ClockOpacity = new DoubleAnimation + { + From = 0, + To = 1, + EasingFunction = easing, + Duration = TimeSpan.FromMilliseconds(animationLength), + FillBehavior = FillBehavior.HoldEnd + }; + + double TargetIconOpacity = GetOpacityFromStyle(SearchIcon.Style, 1.0); + + var IconOpacity = new DoubleAnimation + { + From = 0, + To = TargetIconOpacity, + EasingFunction = easing, + Duration = TimeSpan.FromMilliseconds(animationLength), + FillBehavior = FillBehavior.HoldEnd + }; + + double rightMargin = GetThicknessFromStyle(ClockPanel.Style, new Thickness(0, 0, DefaultRightMargin, 0)).Right; + + var thicknessAnimation = new ThicknessAnimation + { + From = new Thickness(0, 12, rightMargin, 0), + To = new Thickness(0, 0, rightMargin, 0), + EasingFunction = easing, + Duration = TimeSpan.FromMilliseconds(animationLength), + FillBehavior = FillBehavior.HoldEnd + }; + + Storyboard.SetTargetProperty(ClockOpacity, new PropertyPath(OpacityProperty)); + Storyboard.SetTarget(ClockOpacity, ClockPanel); + + Storyboard.SetTargetName(thicknessAnimation, "ClockPanel"); + Storyboard.SetTargetProperty(thicknessAnimation, new PropertyPath(MarginProperty)); + + Storyboard.SetTarget(IconMotion, SearchIcon); + Storyboard.SetTargetProperty(IconMotion, new PropertyPath(TopProperty)); + + Storyboard.SetTarget(IconOpacity, SearchIcon); + Storyboard.SetTargetProperty(IconOpacity, new PropertyPath(OpacityProperty)); + + clocksb.Children.Add(thicknessAnimation); + clocksb.Children.Add(ClockOpacity); + iconsb.Children.Add(IconMotion); + iconsb.Children.Add(IconOpacity); + + clocksb.Completed += (_, _) => _animating = false; + _settings.WindowLeft = Left; + isArrowKeyPressed = false; + + if (QueryTextBox.Text.Length == 0) + { + clocksb.Begin(ClockPanel); + } + + iconsb.Begin(SearchIcon); + } + + private void UpdateClockPanelVisibility() + { + if (QueryTextBox == null || ContextMenu == null || History == null || ClockPanel == null) + return; + + var animationLength = _settings.AnimationSpeed switch + { + AnimationSpeeds.Slow => 560, + AnimationSpeeds.Medium => 360, + AnimationSpeeds.Fast => 160, + _ => _settings.CustomAnimationLength + }; + + var animationDuration = TimeSpan.FromMilliseconds(animationLength * 2 / 3); + + // ✅ Conditions for showing ClockPanel (No query input & ContextMenu, History are closed) + bool shouldShowClock = QueryTextBox.Text.Length == 0 && + ContextMenu.Visibility != Visibility.Visible && + History.Visibility != Visibility.Visible; + + // ✅ 1. When ContextMenu opens, immediately set Visibility.Hidden (force hide without animation) + if (ContextMenu.Visibility == Visibility.Visible) + { + ClockPanel.Visibility = Visibility.Hidden; + ClockPanel.Opacity = 0.0; // Set to 0 in case Opacity animation affects it + return; + } + + // ✅ 2. When ContextMenu is closed, keep it Hidden if there's text in the query (remember previous state) + if (ContextMenu.Visibility != Visibility.Visible && QueryTextBox.Text.Length > 0) + { + ClockPanel.Visibility = Visibility.Hidden; + ClockPanel.Opacity = 0.0; + return; + } + + // ✅ 3. When hiding ClockPanel (apply fade-out animation) + if ((!shouldShowClock) && ClockPanel.Visibility == Visibility.Visible && !_isClockPanelAnimating) + { + _isClockPanelAnimating = true; + + var fadeOut = new DoubleAnimation + { + From = 1.0, + To = 0.0, + Duration = animationDuration, + FillBehavior = FillBehavior.HoldEnd + }; + + fadeOut.Completed += (s, e) => + { + ClockPanel.Visibility = Visibility.Hidden; // ✅ Completely hide after animation + _isClockPanelAnimating = false; + }; + + ClockPanel.BeginAnimation(OpacityProperty, fadeOut); + } + // ✅ 4. When showing ClockPanel (apply fade-in animation) + else if (shouldShowClock && ClockPanel.Visibility != Visibility.Visible && !_isClockPanelAnimating) + { + _isClockPanelAnimating = true; + + Application.Current.Dispatcher.Invoke(() => + { + ClockPanel.Visibility = Visibility.Visible; // ✅ Set Visibility to Visible first + + var fadeIn = new DoubleAnimation + { + From = 0.0, + To = 1.0, + Duration = animationDuration, + FillBehavior = FillBehavior.HoldEnd + }; + + fadeIn.Completed += (s, e) => _isClockPanelAnimating = false; + ClockPanel.BeginAnimation(OpacityProperty, fadeIn); + }, DispatcherPriority.Render); + } + } + + + private static double GetOpacityFromStyle(Style style, double defaultOpacity = 1.0) + { + if (style == null) + return defaultOpacity; + + foreach (Setter setter in style.Setters.Cast()) + { + if (setter.Property == OpacityProperty) + { + return setter.Value is double opacity ? opacity : defaultOpacity; + } + } + + return defaultOpacity; + } + + private static Thickness GetThicknessFromStyle(Style style, Thickness defaultThickness) + { + if (style == null) + return defaultThickness; + + foreach (Setter setter in style.Setters.Cast()) + { + if (setter.Property == MarginProperty) + { + return setter.Value is Thickness thickness ? thickness : defaultThickness; + } + } + + return defaultThickness; + } + + #endregion + + #region QueryTextBox Event + + private void QueryTextBox_OnCopy(object sender, ExecutedRoutedEventArgs e) + { + var result = _viewModel.Results.SelectedItem?.Result; + if (QueryTextBox.SelectionLength == 0 && result != null) + { + string copyText = result.CopyText; + App.API.CopyToClipboard(copyText, directCopy: true); + } + else if (!string.IsNullOrEmpty(QueryTextBox.Text)) + { + App.API.CopyToClipboard(QueryTextBox.SelectedText, showDefaultNotification: false); + } + } + + private void QueryTextBox_OnPaste(object sender, DataObjectPastingEventArgs e) + { + var isText = e.SourceDataObject.GetDataPresent(DataFormats.UnicodeText, true); + if (isText) + { + var text = e.SourceDataObject.GetData(DataFormats.UnicodeText) as string; + text = text.Replace(Environment.NewLine, " "); + DataObject data = new DataObject(); + data.SetData(DataFormats.UnicodeText, text); + e.DataObject = data; } } @@ -853,11 +991,18 @@ namespace Flow.Launcher { if (_viewModel.QueryText != QueryTextBox.Text) { - BindingExpression be = QueryTextBox.GetBindingExpression(System.Windows.Controls.TextBox.TextProperty); + BindingExpression be = QueryTextBox.GetBindingExpression(TextBox.TextProperty); be.UpdateSource(); } } - + + private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e) + { + e.Handled = true; + } + + #endregion + #region Search Delay // Edited from: https://github.com/microsoft/PowerToys diff --git a/Flow.Launcher/Msg.xaml.cs b/Flow.Launcher/Msg.xaml.cs index 0bb02bbc5..94184ff63 100644 --- a/Flow.Launcher/Msg.xaml.cs +++ b/Flow.Launcher/Msg.xaml.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.IO; using System.Windows; using System.Windows.Forms; using System.Windows.Input; using System.Windows.Media.Animation; -using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Image; @@ -12,14 +11,14 @@ namespace Flow.Launcher { public partial class Msg : Window { - Storyboard fadeOutStoryboard = new Storyboard(); + private readonly Storyboard fadeOutStoryboard = new(); private bool closing; public Msg() { InitializeComponent(); var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var dipWorkingArea = WindowsInteropHelper.TransformPixelsToDIP(this, + var dipWorkingArea = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, screen.WorkingArea.Height); Left = dipWorkingArea.X - Width; @@ -82,13 +81,13 @@ namespace Flow.Launcher Show(); await Dispatcher.InvokeAsync(async () => - { - if (!closing) - { - closing = true; - await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin); - } - }); + { + if (!closing) + { + closing = true; + await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin); + } + }); } } } diff --git a/Flow.Launcher/NativeMethods.txt b/Flow.Launcher/NativeMethods.txt deleted file mode 100644 index 88eeeca6e..000000000 --- a/Flow.Launcher/NativeMethods.txt +++ /dev/null @@ -1,20 +0,0 @@ -DwmSetWindowAttribute -DwmExtendFrameIntoClientArea -SystemParametersInfo -SetForegroundWindow - -GetWindowLong -SetWindowLong -GetForegroundWindow -GetDesktopWindow -GetShellWindow -GetWindowRect -GetClassName -FindWindowEx -WINDOW_STYLE - -WM_ENTERSIZEMOVE -WM_EXITSIZEMOVE - -SetLastError -WINDOW_EX_STYLE \ No newline at end of file diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index ac22170ae..456f1ad47 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -1,46 +1,48 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Net; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using System.Windows; +using CommunityToolkit.Mvvm.DependencyInjection; using Squirrel; +using Flow.Launcher.Core; using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; -using Flow.Launcher.Plugin; -using Flow.Launcher.ViewModel; -using Flow.Launcher.Plugin.SharedModels; -using Flow.Launcher.Plugin.SharedCommands; -using System.Threading; -using System.IO; -using Flow.Launcher.Infrastructure.Http; -using JetBrains.Annotations; -using System.Runtime.CompilerServices; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Collections.Specialized; -using CommunityToolkit.Mvvm.DependencyInjection; -using Flow.Launcher.Core; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; +using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.ViewModel; +using JetBrains.Annotations; +using Flow.Launcher.Core.Resource; namespace Flow.Launcher { public class PublicAPIInstance : IPublicAPI { private readonly Settings _settings; + private readonly Internationalization _translater; private readonly MainViewModel _mainVM; #region Constructor - public PublicAPIInstance(Settings settings, MainViewModel mainVM) + public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM) { _settings = settings; + _translater = translater; _mainVM = mainVM; GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback; WebRequest.RegisterPrefix("data", new DataWebRequestFactory()); @@ -153,17 +155,17 @@ namespace Flow.Launcher public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; - public string GetTranslation(string key) => InternationalizationManager.Instance.GetTranslation(key); + public string GetTranslation(string key) => _translater.GetTranslation(key); public List GetAllPlugins() => PluginManager.AllPlugins.ToList(); public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); - public Task HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url); + public Task HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url, token); public Task HttpGetStreamAsync(string url, CancellationToken token = default) => - Http.GetStreamAsync(url); + Http.GetStreamAsync(url, token); public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default) => Http.DownloadAsync(url, filePath, reportProgress, token); @@ -329,7 +331,6 @@ namespace Flow.Launcher return _mainVM.GameModeStatus; } - private readonly List> _globalKeyboardHandlers = new(); public void RegisterGlobalKeyboardCallback(Func callback) => _globalKeyboardHandlers.Add(callback); diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml index ff2f14c4b..ded6e0e27 100644 --- a/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml @@ -11,30 +11,33 @@ mc:Ignorable="d"> - + + - +