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..aab3caa40 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -205,9 +205,6 @@ namespace Flow.Launcher.Core.Plugin } } - InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface()); - InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService().Language); - if (failedPlugins.Any()) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); @@ -231,7 +228,6 @@ namespace Flow.Launcher.Core.Plugin if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword)) return GlobalPlugins; - var plugin = NonGlobalPlugins[query.ActionKeyword]; return new List { @@ -367,7 +363,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 +393,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/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index e2a66656a..ffa17ab4d 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -67,9 +67,9 @@ namespace Flow.Launcher.Core.Resource return DefaultLanguageCode; } - internal void AddPluginLanguageDirectories(IEnumerable plugins) + private void AddPluginLanguageDirectories() { - foreach (var plugin in plugins) + foreach (var plugin in PluginManager.GetPluginsForInterface()) { var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location; var dir = Path.GetDirectoryName(location); @@ -96,6 +96,32 @@ namespace Flow.Launcher.Core.Resource _oldResources.Clear(); } + /// + /// Initialize language. Will change app language and plugin language based on settings. + /// + public async Task InitializeLanguageAsync() + { + // Get actual language + var languageCode = _settings.Language; + if (languageCode == Constant.SystemLanguageCode) + { + languageCode = SystemLanguageCode; + } + + // Get language by language code and change language + var language = GetLanguageByLanguageCode(languageCode); + + // Add plugin language directories first so that we can load language files from plugins + AddPluginLanguageDirectories(); + + // Change language + await ChangeLanguageAsync(language); + } + + /// + /// Change language during runtime. Will change app language and plugin language & save settings. + /// + /// public void ChangeLanguage(string languageCode) { languageCode = languageCode.NonNull(); @@ -110,7 +136,12 @@ namespace Flow.Launcher.Core.Resource // Get language by language code and change language var language = GetLanguageByLanguageCode(languageCode); - ChangeLanguage(language, isSystem); + + // Change language + _ = ChangeLanguageAsync(language); + + // Save settings + _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } private Language GetLanguageByLanguageCode(string languageCode) @@ -128,26 +159,22 @@ namespace Flow.Launcher.Core.Resource } } - private void ChangeLanguage(Language language, bool isSystem) + private async Task ChangeLanguageAsync(Language language) { - language = language.NonNull(); - + // Remove old language files and load language RemoveOldLanguageFiles(); if (language != AvailableLanguages.English) { LoadLanguage(language); } + // Culture of main thread // Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode); CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture; - // Raise event after culture is set - _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; - _ = Task.Run(() => - { - UpdatePluginMetadataTranslations(); - }); + // Raise event for plugins after culture is set + await Task.Run(UpdatePluginMetadataTranslations); } public bool PromptShouldUsePinyin(string languageCodeToSet) diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index cda125a39..e5980b62f 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.Controls.Primitives; 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; namespace Flow.Launcher.Core.Resource { public class Theme { + #region Properties & Fields + + public bool BlurEnabled { get; private set; } + private const string ThemeMetadataNamePrefix = "Name:"; private const string ThemeMetadataIsDarkPrefix = "IsDark:"; private const string ThemeMetadataHasBlurPrefix = "HasBlur:"; @@ -31,14 +39,14 @@ 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; + private Thickness _themeResizeBorderThickness; - public bool BlurEnabled { get; set; } + #endregion - private double mainWindowWidth; + #region Constructor public Theme(IPublicAPI publicAPI, Settings settings) { @@ -50,20 +58,32 @@ namespace Flow.Launcher.Core.Resource MakeSureThemeDirectoriesExist(); var dicts = Application.Current.Resources.MergedDictionaries; - _oldResource = dicts.First(d => + _oldResource = dicts.FirstOrDefault(d => { - if (d.Source == null) - return false; + if (d.Source == null) return false; var p = d.Source.AbsolutePath; - var dir = Path.GetDirectoryName(p).NonNull(); - var info = new DirectoryInfo(dir); - var f = info.Name; - var e = Path.GetExtension(p); - var found = f == Folder && e == Extension; - return found; + return p.Contains(Folder) && Path.GetExtension(p) == Extension; }); - _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + + if (_oldResource != null) + { + _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + } + else + { + Log.Error("Current theme resource not found. Initializing with default theme."); + _oldTheme = Constant.DefaultTheme; + }; + } + + #endregion + + #region Theme Resources + + public string GetCurrentTheme() + { + return _settings.Theme; } private void MakeSureThemeDirectoriesExist() @@ -81,68 +101,154 @@ 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; + // Add new resources + if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate)) + { + Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate); + } + + // Remove old resources + if (_oldResource != null && _oldResource != dictionaryToUpdate && + Application.Current.Resources.MergedDictionaries.Contains(_oldResource)) + { + Application.Current.Resources.MergedDictionaries.Remove(_oldResource); + } - dicts.Remove(_oldResource); - dicts.Add(dictionaryToUpdate); _oldResource = dictionaryToUpdate; } + /// + /// Updates only the font settings and refreshes the UI. + /// + public void UpdateFonts() + { + try + { + // Load a ResourceDictionary for the specified theme. + var themeName = GetCurrentTheme(); + var dict = GetThemeResourceDictionary(themeName); + + // Apply font settings to the theme resource. + ApplyFontSettings(dict); + UpdateResourceDictionary(dict); + + // Must apply blur and drop shadow effects + _ = RefreshFrameAsync(); + } + catch (Exception e) + { + Log.Exception("Error occurred while updating theme fonts", e); + } + } + + /// + /// Loads and applies font settings to the theme resource. + /// + private void ApplyFontSettings(ResourceDictionary dict) + { + if (dict["QueryBoxStyle"] is Style queryBoxStyle && + dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) + { + var fontFamily = new FontFamily(_settings.QueryBoxFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); + + SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true); + SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemTitleStyle"] is Style resultItemStyle && + dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && + dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && + dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch); + + SetFontProperties(resultItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle && + dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultSubFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch); + + SetFontProperties(resultSubItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultSubItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + } + + /// + /// Applies font properties to a Style. + /// + private static void SetFontProperties(Style style, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, bool isTextBox) + { + // Remove existing font-related setters + if (isTextBox) + { + // First, find the setters to remove and store them in a list + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == Control.FontFamilyProperty || + setter.Property == Control.FontStyleProperty || + setter.Property == Control.FontWeightProperty || + setter.Property == Control.FontStretchProperty) + .ToList(); + + // Remove each found setter one by one + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + // Add New font setter + style.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); + + // Set caret brush (retain existing logic) + var caretBrushPropertyValue = style.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); + var foregroundPropertyValue = style.Setters.OfType().Where(x => x.Property.Name == "Foreground") + .Select(x => x.Value).FirstOrDefault(); + if (!caretBrushPropertyValue && foregroundPropertyValue != null) + style.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue)); + } + else + { + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == TextBlock.FontFamilyProperty || + setter.Property == TextBlock.FontStyleProperty || + setter.Property == TextBlock.FontWeightProperty || + setter.Property == TextBlock.FontStretchProperty) + .ToList(); + + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(TextBlock.FontStretchProperty, fontStretch)); + } + } + private ResourceDictionary GetThemeResourceDictionary(string theme) { var uri = GetThemePath(theme); @@ -154,9 +260,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); @@ -168,22 +272,22 @@ namespace Flow.Launcher.Core.Resource var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); + queryBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); + queryBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle)); + queryBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight)); + queryBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); var caretBrushPropertyValue = queryBoxStyle.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); var foregroundPropertyValue = queryBoxStyle.Setters.OfType().Where(x => x.Property.Name == "Foreground") .Select(x => x.Value).FirstOrDefault(); if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling - queryBoxStyle.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue)); + queryBoxStyle.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue)); // Query suggestion box's font style is aligned with query box - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); } if (dict["ItemTitleStyle"] is Style resultItemStyle && @@ -213,36 +317,20 @@ 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))); } /* Ignore Theme Window Width and use setting */ var windowStyle = dict["WindowStyle"] as Style; var width = _settings.WindowSize; - windowStyle.Setters.Add(new Setter(Window.WidthProperty, width)); - mainWindowWidth = (double)width; + windowStyle.Setters.Add(new Setter(FrameworkElement.WidthProperty, 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 +352,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 +381,82 @@ 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}>"); + + // Retrieve theme resource – always use the resource with font settings applied. + var resourceDict = GetResourceDictionary(theme); + + UpdateResourceDictionary(resourceDict); + + _settings.Theme = theme; + + //always allow re-loading default theme, in case of failure of switching to a new theme from default theme + if (_oldTheme != theme || theme == Constant.DefaultTheme) + { + _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + } + + BlurEnabled = IsBlurTheme(); + + // Can only apply blur but here also apply drop shadow effect to avoid possible drop shadow effect issues + _ = RefreshFrameAsync(); + + return true; + } + 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; + } + } + + #endregion + + #region Shadow Effect + public void AddDropShadowEffectToCurrentTheme() { var dict = GetCurrentResourceDictionary(); @@ -301,7 +465,7 @@ namespace Flow.Launcher.Core.Resource var effectSetter = new Setter { - Property = Border.EffectProperty, + Property = UIElement.EffectProperty, Value = new DropShadowEffect { Opacity = 0.3, @@ -311,13 +475,12 @@ 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 == FrameworkElement.MarginProperty) is not Setter marginSetter) { var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin); marginSetter = new Setter() { - Property = Border.MarginProperty, + Property = FrameworkElement.MarginProperty, Value = margin, }; windowBorderStyle.Setters.Add(marginSetter); @@ -347,14 +510,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 == UIElement.EffectProperty) is Setter effectSetter) { windowBorderStyle.Setters.Remove(effectSetter); } - if (marginSetter != null) + + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is Setter marginSetter) { var currentMargin = (Thickness)marginSetter.Value; var newMargin = new Thickness( @@ -370,31 +531,378 @@ namespace Flow.Launcher.Core.Resource UpdateResourceDictionary(dict); } + public void SetResizeBorderThickness(WindowChrome windowChrome, bool fixedWindowSize) + { + if (fixedWindowSize) + { + windowChrome.ResizeBorderThickness = new Thickness(0); + } + else + { + windowChrome.ResizeBorderThickness = _themeResizeBorderThickness; + } + } + // because adding drop shadow effect will change the margin of the window, // we need to update the window chrome thickness to correct set the resize border - private static void SetResizeBoarderThickness(Thickness? effectMargin) + private void SetResizeBoarderThickness(Thickness? effectMargin) { var window = Application.Current.MainWindow; if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome) { - Thickness thickness; + // Save the theme resize border thickness so that we can restore it if we change ResizeWindow setting if (effectMargin == null) { - thickness = SystemParameters.WindowResizeBorderThickness; + _themeResizeBorderThickness = SystemParameters.WindowResizeBorderThickness; } else { - thickness = new Thickness( + _themeResizeBorderThickness = new Thickness( effectMargin.Value.Left + SystemParameters.WindowResizeBorderThickness.Left, effectMargin.Value.Top + SystemParameters.WindowResizeBorderThickness.Top, effectMargin.Value.Right + SystemParameters.WindowResizeBorderThickness.Right, effectMargin.Value.Bottom + SystemParameters.WindowResizeBorderThickness.Bottom); } - windowChrome.ResizeBorderThickness = thickness; + // Apply the resize border thickness to the window chrome + SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults); } } + #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.Render); + } + + /// + /// 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.Render); + } + + /// + /// 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 = GetResourceDictionary(theme); + if (dict == null) return; + + var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null; + if (windowBorderStyle == 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; + 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.Core/Resource/ThemeManager.cs b/Flow.Launcher.Core/Resource/ThemeManager.cs deleted file mode 100644 index 3cbe8319a..000000000 --- a/Flow.Launcher.Core/Resource/ThemeManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.DependencyInjection; - -namespace Flow.Launcher.Core.Resource -{ - [Obsolete("ThemeManager.Instance is obsolete. Use Ioc.Default.GetRequiredService() instead.")] - public class ThemeManager - { - public static Theme Instance - => Ioc.Default.GetRequiredService(); - } -} diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index c86ed4324..b4b2485c9 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -32,6 +32,7 @@ namespace Flow.Launcher.Infrastructure public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png"); public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png"); public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png"); + public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png"); public static string PythonPath; public static string NodePath; diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs index b97c096c3..1085cc833 100644 --- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -67,8 +67,6 @@ namespace Flow.Launcher.Infrastructure return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First; } - private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - /// /// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1. /// 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..363ecb9d0 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -16,4 +16,46 @@ 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 + +GetKeyboardLayout +GetWindowThreadProcessId +ActivateKeyboardLayout +GetKeyboardLayoutList +PostMessage +WM_INPUTLANGCHANGEREQUEST +INPUTLANGCHANGE_FORWARD +LOCALE_TRANSIENT_KEYBOARD1 +LOCALE_TRANSIENT_KEYBOARD2 +LOCALE_TRANSIENT_KEYBOARD3 +LOCALE_TRANSIENT_KEYBOARD4 \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/PInvokeExtensions.cs b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs new file mode 100644 index 000000000..1a72ab7a6 --- /dev/null +++ b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Windows.Win32; + +// Edited from: https://github.com/files-community/Files +internal static partial class PInvoke +{ + [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)] + static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); + + [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)] + static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); + + // NOTE: + // CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa. + // For more info, visit https://github.com/microsoft/CsWin32/issues/882 + public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong) + { + return sizeof(nint) is 4 + ? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong) + : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong); + } +} diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 93f6db111..bd146f49a 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -68,14 +68,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings get => _theme; set { - if (value == _theme) - return; - _theme = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(MaxResultsToShow)); + if (value != _theme) + { + _theme = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(MaxResultsToShow)); + } } } 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; @@ -112,6 +114,33 @@ namespace Flow.Launcher.Infrastructure.UserSettings public double? SettingWindowLeft { get; set; } = null; public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal; + bool _showPlaceholder { get; set; } = false; + public bool ShowPlaceholder + { + get => _showPlaceholder; + set + { + if (_showPlaceholder != value) + { + _showPlaceholder = value; + OnPropertyChanged(); + } + } + } + string _placeholderText { get; set; } = string.Empty; + public string PlaceholderText + { + get => _placeholderText; + set + { + if (_placeholderText != value) + { + _placeholderText = value; + OnPropertyChanged(); + } + } + } + public int CustomExplorerIndex { get; set; } = 0; [JsonIgnore] @@ -240,10 +269,26 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// public double CustomWindowTop { get; set; } = 0; - public bool KeepMaxResults { get; set; } = false; - public int MaxResultsToShow { get; set; } = 5; - public int ActivateTimes { get; set; } + /// + /// Fixed window size + /// + private bool _keepMaxResults { get; set; } = false; + public bool KeepMaxResults + { + get => _keepMaxResults; + set + { + if (_keepMaxResults != value) + { + _keepMaxResults = value; + OnPropertyChanged(); + } + } + } + public int MaxResultsToShow { get; set; } = 5; + + public int ActivateTimes { get; set; } public ObservableCollection CustomPluginHotkeys { get; set; } = new ObservableCollection(); @@ -430,4 +475,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..7a3a0c36e 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -1,7 +1,18 @@ using System; +using System.ComponentModel; +using System.Globalization; using System.Runtime.InteropServices; -using System.Windows.Interop; using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using Flow.Launcher.Infrastructure.UserSettings; +using Microsoft.Win32; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Dwm; +using Windows.Win32.UI.Input.KeyboardAndMouse; +using Windows.Win32.UI.WindowsAndMessaging; +using Point = System.Windows.Point; namespace Flow.Launcher.Infrastructure { @@ -9,93 +20,473 @@ 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 + + /// + /// 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)); + } + + #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 + + #region Keyboard Layout + + private const string UserProfileRegistryPath = @"Control Panel\International\User Profile"; + + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f + private const string EnglishLanguageTag = "en"; + + private static readonly string[] ImeLanguageTags = + { + "zh", // Chinese + "ja", // Japanese + "ko", // Korean + }; + + private const uint KeyboardLayoutLoWord = 0xFFFF; + + // Store the previous keyboard layout + private static HKL _previousLayout; + + /// + /// Switches the keyboard layout to English if available. + /// + /// If true, the current keyboard layout will be stored for later restoration. + /// Thrown when there's an error getting the window thread process ID. + public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious) + { + // Find an installed English layout + var enHKL = FindEnglishKeyboardLayout(); + + // No installed English layout found + if (enHKL == HKL.Null) return; + + // Get the current foreground window + var hwnd = PInvoke.GetForegroundWindow(); + if (hwnd == HWND.Null) return; + + // Get the current foreground window thread ID + var threadId = PInvoke.GetWindowThreadProcessId(hwnd); + if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + + // If the current layout has an IME mode, disable it without switching to another layout. + // This is needed because for languages with IME mode, Flow Launcher just temporarily disables + // the IME mode instead of switching to another layout. + var currentLayout = PInvoke.GetKeyboardLayout(threadId); + var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord; + foreach (var langTag in ImeLanguageTags) + { + if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + + // Backup current keyboard layout + if (backupPrevious) _previousLayout = currentLayout; + + // Switch to English layout + PInvoke.ActivateKeyboardLayout(enHKL, 0); } /// - /// Sets the blur for a window via SetWindowCompositionAttribute + /// Restores the previously backed-up keyboard layout. + /// If it wasn't backed up or has already been restored, this method does nothing. /// - public static void SetBlurForWindow(Window w, bool blur) + public static void RestorePreviousKeyboardLayout() { - SetWindowAccent(w, blur ? AccentState.ACCENT_ENABLE_BLURBEHIND : AccentState.ACCENT_DISABLED); + if (_previousLayout == HKL.Null) return; + + var hwnd = PInvoke.GetForegroundWindow(); + if (hwnd == HWND.Null) return; + + PInvoke.PostMessage( + hwnd, + PInvoke.WM_INPUTLANGCHANGEREQUEST, + PInvoke.INPUTLANGCHANGE_FORWARD, + _previousLayout.Value + ); + + _previousLayout = HKL.Null; } - private static void SetWindowAccent(Window w, AccentState state) + /// + /// Finds an installed English keyboard layout. + /// + /// + /// + private static unsafe HKL FindEnglishKeyboardLayout() { - var windowHelper = new WindowInteropHelper(w); + // Get the number of keyboard layouts + int count = PInvoke.GetKeyboardLayoutList(0, null); + if (count <= 0) return HKL.Null; - 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 + // Get all keyboard layouts + var handles = new HKL[count]; + fixed (HKL* h = handles) { - Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY, - SizeOfData = accentStructSize, - Data = accentPtr - }; + var result = PInvoke.GetKeyboardLayoutList(count, h); + if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + } - SetWindowCompositionAttribute(windowHelper.Handle, ref data); + // Look for any English keyboard layout + foreach (var hkl in handles) + { + // The lower word contains the language identifier + var langId = (uint)hkl.Value & KeyboardLayoutLoWord; + var langTag = GetLanguageTag(langId); - Marshal.FreeHGlobal(accentPtr); + // Check if it's an English layout + if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase)) + { + return hkl; + } + } + + return HKL.Null; } + + /// + /// Returns the + /// + /// BCP 47 language tag + /// + /// of the current input language. + /// + /// + /// Edited from: https://github.com/dotnet/winforms + /// + private static string GetLanguageTag(uint langId) + { + // We need to convert the language identifier to a language tag, because they are deprecated and may have a + // transient value. + // https://learn.microsoft.com/globalization/locale/other-locale-names#lcid + // https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks + // + // It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect + // language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID" + // instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet). + // + // Try to extract proper language tag from registry as a workaround approved by a Windows team. + // https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949 + // + // NOTE: this logic may break in future versions of Windows since it is not documented. + if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD2 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD3 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD4) + { + using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath); + if (key?.GetValue("Languages") is string[] languages) + { + foreach (string language in languages) + { + using var subKey = key.OpenSubKey(language); + if (subKey?.GetValue("TransientLangId") is int transientLangId + && transientLangId == langId) + { + return language; + } + } + } + } + + return CultureInfo.GetCultureInfo((int)langId).Name; + } + #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 b4e06913e..91256298b 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -34,6 +34,8 @@ namespace Flow.Launcher.Plugin public List ActionKeywords { get; set; } + public bool HideActionKeywordPanel { get; set; } + public string IcoPath { get; set;} public override string ToString() 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..7b1d113fb 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -27,11 +27,26 @@ namespace Flow.Launcher { public partial class App : IDisposable, ISingleInstanceApp { + #region Public Properties + public static IPublicAPI API { get; private set; } - private const string Unique = "Flow.Launcher_Unique_Application_Mutex"; + + #endregion + + #region Private Fields + private static bool _disposed; + private MainWindow _mainWindow; + private readonly MainViewModel _mainVM; private readonly Settings _settings; + // To prevent two disposals running at the same time. + private static readonly object _disposingLock = new(); + + #endregion + + #region Constructor + public App() { // Initialize settings @@ -79,37 +94,47 @@ namespace Flow.Launcher { API = Ioc.Default.GetRequiredService(); _settings.Initialize(); + _mainVM = Ioc.Default.GetRequiredService(); } catch (Exception e) { ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e); return; } + + // Local function + static void ShowErrorMsgBoxAndFailFast(string message, Exception e) + { + // Firstly show users the message + MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); + + // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. + Environment.FailFast(message, e); + } } - private static void ShowErrorMsgBoxAndFailFast(string message, Exception e) - { - // Firstly show users the message - MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); + #endregion - // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. - Environment.FailFast(message, e); - } + #region Main [STAThread] public static void Main() { - if (SingleInstance.InitializeAsFirstInstance(Unique)) + if (SingleInstance.InitializeAsFirstInstance()) { - 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) + #endregion + + #region App Events + +#pragma warning disable VSTHRD100 // Avoid async void methods + + private async void OnStartup(object sender, StartupEventArgs e) { await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => { @@ -127,29 +152,32 @@ namespace Flow.Launcher AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); - // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future - InternationalizationManager.Instance.ChangeLanguage(_settings.Language); - PluginManager.LoadPlugins(_settings.PluginSettings); + // Register ResultsUpdated event after all plugins are loaded + Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); + Http.Proxy = _settings.Proxy; await PluginManager.InitializePluginsAsync(); + + // Change language after all plugins are initialized because we need to update plugin title based on their api + // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future + await Ioc.Default.GetRequiredService().InitializeLanguageAsync(); + await imageLoadertask; - var mainVM = Ioc.Default.GetRequiredService(); - var window = new MainWindow(_settings, mainVM); + _mainWindow = new MainWindow(); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); - Current.MainWindow = window; + Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; HotKeyMapper.Initialize(); // 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); @@ -159,11 +187,12 @@ namespace Flow.Launcher AutoUpdates(); API.SaveAppAllSettings(); - Log.Info( - "|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); + Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------"); }); } +#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,13 +215,11 @@ 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); } } } - //[Conditional("RELEASE")] private void AutoUpdates() { _ = Task.Run(async () => @@ -210,11 +237,29 @@ namespace Flow.Launcher }); } + #endregion + + #region Register Events + private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose(); - Current.Exit += (s, e) => Dispose(); - Current.SessionEnding += (s, e) => Dispose(); + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Process Exit"); + Dispose(); + }; + + Current.Exit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Application Exit"); + Dispose(); + }; + + Current.SessionEnding += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Session Ending"); + Dispose(); + }; } /// @@ -235,20 +280,60 @@ namespace Flow.Launcher AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; } - public void Dispose() + #endregion + + #region IDisposable + + protected virtual void Dispose(bool disposing) { - // if sessionending is called, exit proverbially be called when log off / shutdown - // but if sessionending is not called, exit won't be called when log off / shutdown - if (!_disposed) + // Prevent two disposes at the same time. + lock (_disposingLock) { - API.SaveAppAllSettings(); + if (!disposing) + { + return; + } + + if (_disposed) + { + return; + } + _disposed = true; } + + Stopwatch.Normal("|App.Dispose|Dispose cost", () => + { + Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); + + if (disposing) + { + // Dispose needs to be called on the main Windows thread, + // since some resources owned by the thread need to be disposed. + _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); + _mainVM?.Dispose(); + } + + Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); + }); } + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + + #region ISingleInstanceApp + public void OnSecondAppStarted() { Ioc.Default.GetRequiredService().Show(); } + + #endregion } } diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml index ccf4de870..0171e6d79 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml @@ -58,9 +58,9 @@ - + - + @@ -99,11 +99,12 @@ Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" - Margin="10,0,10,0" + Margin="10 0 10 0" HorizontalAlignment="Left" VerticalAlignment="Center" HorizontalContentAlignment="Left" - DefaultHotkey="" /> + DefaultHotkey="" + Type="CustomQueryHotkey" /> @@ -132,21 +133,21 @@ + BorderThickness="0 1 0 0"> /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance( string uniqueName ) + public static bool InitializeAsFirstInstance() { // Build unique application Id and the IPC channel name. - string applicationIdentifier = uniqueName + Environment.UserName; + string applicationIdentifier = InstanceMutexName + Environment.UserName; - string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); + string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); // Create mutex based on unique application Id to check if this is the first instance of the application. - bool firstInstance; - singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance); + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); if (firstInstance) { - _ = CreateRemoteService(channelName); + _ = CreateRemoteServiceAsync(channelName); return true; } else { - _ = SignalFirstInstance(channelName); + _ = SignalFirstInstanceAsync(channelName); return false; } } @@ -81,7 +79,7 @@ namespace Flow.Launcher.Helper /// public static void Cleanup() { - singleInstanceMutex?.ReleaseMutex(); + SingleInstanceMutex?.ReleaseMutex(); } #endregion @@ -93,22 +91,19 @@ namespace Flow.Launcher.Helper /// Once receives signal from client, will activate first instance. /// /// Application's IPC channel name. - private static async Task CreateRemoteService(string channelName) + private static async Task CreateRemoteServiceAsync(string channelName) { - using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In)) + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) { - while(true) - { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - if (Application.Current != null) - { - // Do an asynchronous call to ActivateFirstInstance function - Application.Current.Dispatcher.Invoke(ActivateFirstInstance); - } - // Disconect client - pipeServer.Disconnect(); - } + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(); + + // Do an asynchronous call to ActivateFirstInstance function + Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); + + // Disconect client + pipeServer.Disconnect(); } } @@ -119,25 +114,13 @@ namespace Flow.Launcher.Helper /// /// Command line arguments for the second instance, passed to the first instance to take appropriate action. /// - private static async Task SignalFirstInstance(string channelName) + private static async Task SignalFirstInstanceAsync(string channelName) { // Create a client pipe connected to server - using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out)) - { - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } - } + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); - /// - /// Callback for activating first instance of the application. - /// - /// Callback argument. - /// Always null. - private static object ActivateFirstInstanceCallback(object o) - { - ActivateFirstInstance(); - return null; + // Connect to the available pipe + await pipeClient.ConnectAsync(0); } /// diff --git a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs index a3bd83a97..77bebe2d3 100644 --- a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs +++ b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs @@ -2,22 +2,19 @@ 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(); + private const int MaxCacheSize = 3; + private static readonly Dictionary<(string, DateTime), ImageBrush> WallpaperCache = new(); + private static readonly object CacheLock = new(); public static Brush GetWallpaperBrush() { @@ -29,47 +26,72 @@ public static class WallpaperPathRetrieval try { - var wallpaperPath = GetWallpaperPath(); - if (wallpaperPath is not null && File.Exists(wallpaperPath)) + var wallpaperPath = Win32Helper.GetWallpaperPath(); + if (string.IsNullOrEmpty(wallpaperPath) || !File.Exists(wallpaperPath)) { - // Since the wallpaper file name can be the same (TranscodedWallpaper), - // we need to add the last modified date to differentiate them - var dateModified = File.GetLastWriteTime(wallpaperPath); - wallpaperCache.TryGetValue((wallpaperPath, dateModified), out var cachedWallpaper); + App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Wallpaper path is invalid: {wallpaperPath}"); + var wallpaperColor = GetWallpaperColor(); + return new SolidColorBrush(wallpaperColor); + } + + // Since the wallpaper file name can be the same (TranscodedWallpaper), + // we need to add the last modified date to differentiate them + var dateModified = File.GetLastWriteTime(wallpaperPath); + lock (CacheLock) + { + WallpaperCache.TryGetValue((wallpaperPath, dateModified), out var cachedWallpaper); if (cachedWallpaper != null) { return cachedWallpaper; } + } + + using var fileStream = File.OpenRead(wallpaperPath); + var decoder = BitmapDecoder.Create(fileStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None); + var frame = decoder.Frames[0]; + var originalWidth = frame.PixelWidth; + var originalHeight = frame.PixelHeight; - // We should not dispose the memory stream since the bitmap is still in use - var memStream = new MemoryStream(File.ReadAllBytes(wallpaperPath)); - var bitmap = new BitmapImage(); - bitmap.BeginInit(); - bitmap.StreamSource = memStream; - bitmap.DecodePixelWidth = 800; - bitmap.DecodePixelHeight = 600; - bitmap.EndInit(); - bitmap.Freeze(); // Make the bitmap thread-safe - var wallpaperBrush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill }; - wallpaperBrush.Freeze(); // Make the brush thread-safe + if (originalWidth == 0 || originalHeight == 0) + { + App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}"); + return new SolidColorBrush(Colors.Transparent); + } - // Manage cache size - if (wallpaperCache.Count >= MAX_CACHE_SIZE) + // Calculate the scaling factor to fit the image within 800x600 while preserving aspect ratio + var widthRatio = 800.0 / originalWidth; + var heightRatio = 600.0 / originalHeight; + var scaleFactor = Math.Min(widthRatio, heightRatio); + var decodedPixelWidth = (int)(originalWidth * scaleFactor); + var decodedPixelHeight = (int)(originalHeight * scaleFactor); + + // Set DecodePixelWidth and DecodePixelHeight to resize the image while preserving aspect ratio + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(wallpaperPath); + bitmap.DecodePixelWidth = decodedPixelWidth; + bitmap.DecodePixelHeight = decodedPixelHeight; + bitmap.EndInit(); + bitmap.Freeze(); // Make the bitmap thread-safe + var wallpaperBrush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill }; + wallpaperBrush.Freeze(); // Make the brush thread-safe + + // Manage cache size + lock (CacheLock) + { + if (WallpaperCache.Count >= MaxCacheSize) { // Remove the oldest wallpaper from the cache - var oldestCache = wallpaperCache.Keys.OrderBy(k => k.Item2).FirstOrDefault(); + var oldestCache = WallpaperCache.Keys.OrderBy(k => k.Item2).FirstOrDefault(); if (oldestCache != default) { - wallpaperCache.Remove(oldestCache); + WallpaperCache.Remove(oldestCache); } } - wallpaperCache.Add((wallpaperPath, dateModified), wallpaperBrush); + WallpaperCache.Add((wallpaperPath, dateModified), wallpaperBrush); return wallpaperBrush; } - - var wallpaperColor = GetWallpaperColor(); - return new SolidColorBrush(wallpaperColor); } catch (Exception ex) { @@ -78,20 +100,9 @@ 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); + RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", false); var result = key?.GetValue("Background", null); if (result is string strResult) { @@ -100,8 +111,9 @@ public static class WallpaperPathRetrieval var parts = strResult.Trim().Split(new[] { ' ' }, 3).Select(byte.Parse).ToList(); return Color.FromRgb(parts[0], parts[1], parts[2]); } - catch + catch (Exception ex) { + App.API.LogException(nameof(WallpaperPathRetrieval), "Error parsing wallpaper color", ex); } } 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 273d18e3f..678280bba 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -84,7 +84,7 @@ namespace Flow.Launcher nameof(Type), typeof(HotkeyType), typeof(HotkeyControl), - new FrameworkPropertyMetadata(HotkeyType.Hotkey, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged) + new FrameworkPropertyMetadata(HotkeyType.None, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged) ); public HotkeyType Type @@ -95,6 +95,10 @@ namespace Flow.Launcher public enum HotkeyType { + None, + // Custom query hotkeys + CustomQueryHotkey, + // Settings hotkeys Hotkey, PreviewHotkey, OpenContextMenuHotkey, @@ -115,12 +119,16 @@ namespace Flow.Launcher // and it will not construct settings instances twice private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + private string hotkey = string.Empty; public string Hotkey { get { return Type switch { + // Custom query hotkeys + HotkeyType.CustomQueryHotkey => hotkey, + // Settings hotkeys HotkeyType.Hotkey => _settings.Hotkey, HotkeyType.PreviewHotkey => _settings.PreviewHotkey, HotkeyType.OpenContextMenuHotkey => _settings.OpenContextMenuHotkey, @@ -135,13 +143,20 @@ namespace Flow.Launcher HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2, HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey, HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2, - _ => string.Empty + _ => throw new System.NotImplementedException("Hotkey type not set") }; } set { switch (Type) { + // Custom query hotkeys + case HotkeyType.CustomQueryHotkey: + // We just need to store it in a local field + // because we will save to settings in other place + hotkey = value; + break; + // Settings hotkeys case HotkeyType.Hotkey: _settings.Hotkey = value; break; @@ -185,7 +200,7 @@ namespace Flow.Launcher _settings.SelectNextItemHotkey2 = value; break; default: - return; + throw new System.NotImplementedException("Hotkey type not set"); } // After setting the hotkey, we need to refresh the interface @@ -199,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 a3f87cd30..c170c44e4 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -39,7 +39,7 @@ Game Mode Suspend the use of Hotkeys. Position Reset - Reset search window position + Type here to search Settings @@ -72,8 +72,6 @@ Empty last Query Preserve Last Action Keyword Select Last Action Keyword - Fixed Window Height - The window height is not adjustable by dragging. Maximum results shown You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. Ignore hotkeys in fullscreen mode @@ -195,8 +193,19 @@ Custom Clock Date + Backdrop Type + None + Acrylic + Mica + Mica Alt This theme supports two(light/dark) modes. This theme supports Blur Transparent Background. + Show placeholder + Display placeholder when query is empty + Placeholder text + Change placeholder text. Input empty will use: {0} + Fixed Window Size + The window size is not adjustable by dragging. Hotkey @@ -337,9 +346,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 0720501ca..82ac63b7d 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -17,20 +17,20 @@ AllowDrop="True" AllowsTransparency="True" Background="Transparent" + Closed="OnClosed" 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" @@ -215,9 +215,17 @@ - + + - + @@ -289,7 +297,9 @@ + Opacity="{Binding ClockPanelOpacity}" + Style="{DynamicResource ClockPanel}" + Visibility="{Binding ClockPanelVisibility}"> @@ -338,7 +349,7 @@ Y2="0" /> - + + + + 70,13.5,18,13.5 + + 9,0,0,0 + 0,0,9,0 + 0,4.5,0,4.5 + + 9,4.5,0,4.5 + 0,4.5,9,4.5 + + + + 180 + 240 + 150 + + diff --git a/Flow.Launcher/Resources/Dark.xaml b/Flow.Launcher/Resources/Dark.xaml index ed031c939..f1ebba080 100644 --- a/Flow.Launcher/Resources/Dark.xaml +++ b/Flow.Launcher/Resources/Dark.xaml @@ -7,17 +7,17 @@ xmlns:sys="clr-namespace:System;assembly=mscorlib"> - + - - - - + + + + - - - + + + #198F8F8F @@ -115,6 +115,8 @@ + + diff --git a/Flow.Launcher/Resources/Light.xaml b/Flow.Launcher/Resources/Light.xaml index 8fe84588f..12b35971d 100644 --- a/Flow.Launcher/Resources/Light.xaml +++ b/Flow.Launcher/Resources/Light.xaml @@ -7,18 +7,18 @@ xmlns:sys="clr-namespace:System;assembly=mscorlib"> - + - - + + - - - - #198F8F8F + + + + #0C000000 @@ -107,6 +107,7 @@ + diff --git a/Flow.Launcher/Resources/Pages/WelcomePage1.xaml b/Flow.Launcher/Resources/Pages/WelcomePage1.xaml index 1728195bd..32fdb62fc 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage1.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage1.xaml @@ -110,8 +110,8 @@ - - + + diff --git a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml index 04c76d027..f62cec3bf 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml @@ -60,7 +60,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal"> - + diff --git a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml index c898ac9a0..3df4b506e 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml @@ -58,10 +58,10 @@ - + - - + + diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs index ed933678d..e35c978ed 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Windows.Media; +using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; @@ -13,7 +14,6 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; using ModernWpf; -using ThemeManager = Flow.Launcher.Core.Resource.ThemeManager; using ThemeManagerForColorSchemeSwitch = ModernWpf.ThemeManager; namespace Flow.Launcher.SettingPages.ViewModels; @@ -22,45 +22,68 @@ public partial class SettingsPaneThemeViewModel : BaseModel { private const string DefaultFont = "Segoe UI"; public Settings Settings { get; } + private readonly Theme _theme = Ioc.Default.GetRequiredService(); public static string LinkHowToCreateTheme => @"https://flowlauncher.com/docs/#/how-to-create-a-theme"; public static string LinkThemeGallery => "https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438"; + private List _themes; + public List Themes => _themes ??= _theme.LoadAvailableThemes(); + private Theme.ThemeData _selectedTheme; public Theme.ThemeData SelectedTheme { - get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == Settings.Theme); + get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == _theme.GetCurrentTheme()); set { _selectedTheme = value; - ThemeManager.Instance.ChangeTheme(value.FileNameWithoutExtension); + _theme.ChangeTheme(value.FileNameWithoutExtension); - if (ThemeManager.Instance.BlurEnabled && Settings.UseDropShadowEffect) - DropShadowEffect = false; + // Update UI state + OnPropertyChanged(nameof(BackdropType)); + OnPropertyChanged(nameof(IsBackdropEnabled)); + OnPropertyChanged(nameof(IsDropShadowEnabled)); + OnPropertyChanged(nameof(DropShadowEffect)); + + _ = _theme.RefreshFrameAsync(); } } + public bool IsBackdropEnabled + { + get + { + if (!Win32Helper.IsBackdropSupported()) return false; + return SelectedTheme?.HasBlur ?? false; + } + } + + public bool IsDropShadowEnabled => !_theme.BlurEnabled; + public bool DropShadowEffect { get => Settings.UseDropShadowEffect; set { - if (ThemeManager.Instance.BlurEnabled && value) + if (_theme.BlurEnabled) { - App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed")); + // Always DropShadowEffect = true with blur theme + Settings.UseDropShadowEffect = true; return; } + // User can change shadow with non-blur theme. if (value) { - ThemeManager.Instance.AddDropShadowEffectToCurrentTheme(); + _theme.AddDropShadowEffectToCurrentTheme(); } else { - ThemeManager.Instance.RemoveDropShadowEffectFromCurrentTheme(); + _theme.RemoveDropShadowEffectFromCurrentTheme(); } Settings.UseDropShadowEffect = value; + OnPropertyChanged(nameof(DropShadowEffect)); } } @@ -94,12 +117,25 @@ public partial class SettingsPaneThemeViewModel : BaseModel set => Settings.ResultSubItemFontSize = value; } - private List _themes; - public List Themes => _themes ??= ThemeManager.Instance.LoadAvailableThemes(); - public class ColorSchemeData : DropdownDataGeneric { } public List ColorSchemes { get; } = DropdownDataGeneric.GetValues("ColorScheme"); + public string ColorScheme + { + get => Settings.ColorScheme; + set + { + ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme = value switch + { + Constant.Light => ApplicationTheme.Light, + Constant.Dark => ApplicationTheme.Dark, + Constant.System => null, + _ => ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme + }; + Settings.ColorScheme = value; + _ = _theme.RefreshFrameAsync(); + } + } public List TimeFormatList { get; } = new() { @@ -174,6 +210,33 @@ public partial class SettingsPaneThemeViewModel : BaseModel public class AnimationSpeedData : DropdownDataGeneric { } public List AnimationSpeeds { get; } = DropdownDataGeneric.GetValues("AnimationSpeed"); + public class BackdropTypeData : DropdownDataGeneric { } + + public List BackdropTypesList { get; } = + DropdownDataGeneric.GetValues("BackdropTypes"); + + public BackdropTypes BackdropType + { + get => Enum.IsDefined(typeof(BackdropTypes), Settings.BackdropType) + ? Settings.BackdropType + : BackdropTypes.None; + set + { + if (!Enum.IsDefined(typeof(BackdropTypes), value)) + { + value = BackdropTypes.None; + } + + Settings.BackdropType = value; + + // Can only apply blur because drop shadow effect is not supported with backdrop + // So drop shadow effect has been disabled + _ = _theme.SetBlurForWindowAsync(); + + OnPropertyChanged(nameof(IsDropShadowEnabled)); + } + } + public bool UseSound { get => Settings.UseSound; @@ -196,6 +259,23 @@ public partial class SettingsPaneThemeViewModel : BaseModel set => Settings.SoundVolume = value; } + public bool ShowPlaceholder + { + get => Settings.ShowPlaceholder; + set => Settings.ShowPlaceholder = value; + } + + public string PlaceholderTextTip + { + get => string.Format(App.API.GetTranslation("PlaceholderTextTip"), App.API.GetTranslation("queryTextBoxPlaceholder")); + } + + public string PlaceholderText + { + get => Settings.PlaceholderText; + set => Settings.PlaceholderText = value; + } + public bool UseClock { get => Settings.UseClock; @@ -219,37 +299,37 @@ public partial class SettingsPaneThemeViewModel : BaseModel { var results = new List { - new Result + new() { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleExplorer"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"), + Title = App.API.GetTranslation("SampleTitleExplorer"), + SubTitle = App.API.GetTranslation("SampleSubTitleExplorer"), IcoPath = Path.Combine( Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png" ) }, - new Result + new() { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleWebSearch"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"), + Title = App.API.GetTranslation("SampleTitleWebSearch"), + SubTitle = App.API.GetTranslation("SampleSubTitleWebSearch"), IcoPath = Path.Combine( Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png" ) }, - new Result + new() { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProgram"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProgram"), + Title = App.API.GetTranslation("SampleTitleProgram"), + SubTitle = App.API.GetTranslation("SampleSubTitleProgram"), IcoPath = Path.Combine( Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png" ) }, - new Result + new() { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProcessKiller"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProcessKiller"), + Title = App.API.GetTranslation("SampleTitleProcessKiller"), + SubTitle = App.API.GetTranslation("SampleSubTitleProcessKiller"), IcoPath = Path.Combine( Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png" @@ -281,7 +361,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel set { Settings.QueryBoxFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.UpdateFonts(); } } @@ -303,7 +383,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel Settings.QueryBoxFontStretch = value.Stretch.ToString(); Settings.QueryBoxFontWeight = value.Weight.ToString(); Settings.QueryBoxFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.UpdateFonts(); } } @@ -325,7 +405,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel set { Settings.ResultFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.UpdateFonts(); } } @@ -347,7 +427,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel Settings.ResultFontStretch = value.Stretch.ToString(); Settings.ResultFontWeight = value.Weight.ToString(); Settings.ResultFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.UpdateFonts(); } } @@ -355,9 +435,9 @@ public partial class SettingsPaneThemeViewModel : BaseModel { get { - if (Fonts.SystemFontFamilies.Count(o => + if (Fonts.SystemFontFamilies.Any(o => o.FamilyNames.Values != null && - o.FamilyNames.Values.Contains(Settings.ResultSubFont)) > 0) + o.FamilyNames.Values.Contains(Settings.ResultSubFont))) { var font = new FontFamily(Settings.ResultSubFont); return font; @@ -371,7 +451,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel set { Settings.ResultSubFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.UpdateFonts(); } } @@ -392,34 +472,23 @@ public partial class SettingsPaneThemeViewModel : BaseModel Settings.ResultSubFontStretch = value.Stretch.ToString(); Settings.ResultSubFontWeight = value.Weight.ToString(); Settings.ResultSubFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.UpdateFonts(); } } public string ThemeImage => Constant.QueryTextBoxIconImagePath; + public SettingsPaneThemeViewModel(Settings settings) + { + Settings = settings; + } + [RelayCommand] private void OpenThemesFolder() { App.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes)); } - public void UpdateColorScheme() - { - ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme = Settings.ColorScheme switch - { - Constant.Light => ApplicationTheme.Light, - Constant.Dark => ApplicationTheme.Dark, - Constant.System => null, - _ => ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme - }; - } - - public SettingsPaneThemeViewModel(Settings settings) - { - Settings = settings; - } - [RelayCommand] public void Reset() { diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml index 18835259b..5e1ba24ea 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml @@ -30,6 +30,7 @@ VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.ScrollUnit="Pixel"> + + @@ -68,6 +70,7 @@ + - + + + Style="{DynamicResource PreviewWindowBorderStyle}"> @@ -370,17 +374,6 @@ - - - - + + + + + + + + + + + + + - + - + + + + + + + + + + + + @@ -667,9 +718,8 @@ DisplayMemberPath="Display" FontSize="14" ItemsSource="{Binding ColorSchemes}" - SelectedValue="{Binding Settings.ColorScheme}" - SelectedValuePath="Value" - SelectionChanged="Selector_OnSelectionChanged" /> + SelectedValue="{Binding ColorScheme, Mode=TwoWay}" + SelectedValuePath="Value" /> diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs index bf7502e19..22de4fcc0 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs @@ -1,4 +1,3 @@ -using System.Windows.Controls; using System.Windows.Navigation; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.SettingPages.ViewModels; @@ -23,9 +22,4 @@ public partial class SettingsPaneTheme : Page base.OnNavigatedTo(e); } - - private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e) - { - _viewModel.UpdateColorScheme(); - } } diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml index a81d9e0d1..6d629de63 100644 --- a/Flow.Launcher/SettingWindow.xaml +++ b/Flow.Launcher/SettingWindow.xaml @@ -18,6 +18,8 @@ Loaded="OnLoaded" MouseDown="window_MouseDown" ResizeMode="CanResize" + SnapsToDevicePixels="True" + UseLayoutRounding="True" StateChanged="Window_StateChanged" Top="{Binding SettingWindowTop, Mode=TwoWay}" WindowStartupLocation="Manual" @@ -54,6 +56,7 @@ Width="16" Height="16" Margin="10 4 4 4" + RenderOptions.BitmapScalingMode="HighQuality" Source="/Images/app.png" /> - @@ -107,7 +106,7 @@ @@ -204,12 +180,10 @@ + + - @@ -135,8 +138,8 @@ BasedOn="{StaticResource BaseSearchIconStyle}" TargetType="{x:Type Path}"> - - + + + @@ -33,7 +43,9 @@ + Opacity="0.6" + Color="#33000000" /> @@ -112,7 +124,7 @@ - - + + @@ -75,7 +79,7 @@ x:Key="SeparatorStyle" BasedOn="{StaticResource BaseSeparatorStyle}" TargetType="{x:Type Rectangle}"> - + diff --git a/Flow.Launcher/Themes/Darker.xaml b/Flow.Launcher/Themes/Darker.xaml index d1abbe978..7d2614be8 100644 --- a/Flow.Launcher/Themes/Darker.xaml +++ b/Flow.Launcher/Themes/Darker.xaml @@ -29,7 +29,9 @@ - #0e172c + #cc1081 \ No newline at end of file diff --git a/Flow.Launcher/Themes/Ubuntu.xaml b/Flow.Launcher/Themes/Ubuntu.xaml index 3c9e0a125..4007370f9 100644 --- a/Flow.Launcher/Themes/Ubuntu.xaml +++ b/Flow.Launcher/Themes/Ubuntu.xaml @@ -32,7 +32,7 @@ x:Key="QueryBoxStyle" BasedOn="{StaticResource BaseQueryBoxStyle}" TargetType="{x:Type TextBox}"> - + @@ -43,7 +43,7 @@ x:Key="QuerySuggestionBoxStyle" BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}" TargetType="{x:Type TextBox}"> - + @@ -56,15 +56,7 @@ TargetType="{x:Type Border}"> - - - - - - - - - + @@ -96,7 +88,7 @@ TargetType="{x:Type Rectangle}"> - + @@ -174,7 +166,7 @@ x:Key="ClockPanel" BasedOn="{StaticResource ClockPanel}" TargetType="{x:Type StackPanel}"> - + - - - +