From e0721aae1464ba54a2c8fb2591cb7fd15660bc61 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 29 Mar 2025 22:03:30 +0800 Subject: [PATCH 01/47] Add new interfaces --- .../Interfaces/IAsyncEmptyQuery.cs | 23 +++++++++++++++ .../Interfaces/IEmptyQuery.cs | 28 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 Flow.Launcher.Plugin/Interfaces/IAsyncEmptyQuery.cs create mode 100644 Flow.Launcher.Plugin/Interfaces/IEmptyQuery.cs diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncEmptyQuery.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncEmptyQuery.cs new file mode 100644 index 000000000..a18f0848d --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncEmptyQuery.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// Asynchronous Query Model for Flow Launcher When Query Text is Empty + /// + public interface IAsyncEmptyQuery + { + /// + /// Asynchronous Querying When Query Text is Empty + /// + /// + /// If the Querying method requires high IO transmission + /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncEmptyQuery interface + /// + /// Cancel when querying job is obsolete + /// + Task> EmptyQueryAsync(CancellationToken token); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IEmptyQuery.cs b/Flow.Launcher.Plugin/Interfaces/IEmptyQuery.cs new file mode 100644 index 000000000..4ebdcf1fd --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IEmptyQuery.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// Synchronous Query Model for Flow Launcher When Query Text is Empty + /// + /// If the Querying method requires high IO transmission + /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncEmptyQuery interface + /// + /// + public interface IEmptyQuery : IAsyncEmptyQuery + { + /// + /// Querying When Query Text is Empty + /// + /// This method will be called within a Task.Run, + /// so please avoid synchrously wait for long. + /// + /// + /// + List EmptyQuery(); + + Task> IAsyncEmptyQuery.EmptyQueryAsync(CancellationToken token) => Task.Run(EmptyQuery); + } +} From 8bb96d7f5866b5bf90dbf0224137144ccd91bd70 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Fri, 2 May 2025 12:26:14 +0800 Subject: [PATCH 02/47] Fix copy to clipboard STA thread issue & Support retry for copy & Async build-in shortcut model & Fix build shortcuts text replace issue & Fix startup window hide issue (#3314) --- Flow.Launcher.Core/Plugin/QueryBuilder.cs | 6 +- Flow.Launcher.Core/Resource/Theme.cs | 20 +- .../NativeMethods.txt | 5 +- .../UserSettings/CustomShortcutModel.cs | 67 ++++-- .../UserSettings/Settings.cs | 4 +- Flow.Launcher.Infrastructure/Win32Helper.cs | 74 +++++++ Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs | 5 +- Flow.Launcher.Plugin/Query.cs | 7 +- Flow.Launcher/App.xaml.cs | 9 +- Flow.Launcher/Helper/DataWebRequestFactory.cs | 12 +- Flow.Launcher/Helper/ErrorReporting.cs | 18 +- Flow.Launcher/Languages/en.xaml | 1 + Flow.Launcher/MainWindow.xaml.cs | 4 - Flow.Launcher/PublicAPIInstance.cs | 88 ++++++-- .../SettingsPanePluginStoreViewModel.cs | 4 +- .../SettingsPanePluginsViewModel.cs | 33 ++- .../Views/SettingsPanePlugins.xaml | 9 +- .../Views/SettingsPanePlugins.xaml.cs | 31 ++- Flow.Launcher/ViewModel/MainViewModel.cs | 207 ++++++++++++------ 19 files changed, 444 insertions(+), 160 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index 3dc7877ac..0ef3f30f5 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Flow.Launcher.Plugin; @@ -33,7 +33,7 @@ namespace Flow.Launcher.Core.Plugin searchTerms = terms; } - return new Query () + return new Query() { Search = search, RawQuery = rawQuery, @@ -42,4 +42,4 @@ namespace Flow.Launcher.Core.Plugin }; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index a46932c6a..059359694 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -76,7 +76,7 @@ namespace Flow.Launcher.Core.Resource { _api.LogError(ClassName, "Current theme resource not found. Initializing with default theme."); _oldTheme = Constant.DefaultTheme; - }; + } } #endregion @@ -126,7 +126,7 @@ namespace Flow.Launcher.Core.Resource // Load a ResourceDictionary for the specified theme. var themeName = _settings.Theme; var dict = GetThemeResourceDictionary(themeName); - + // Apply font settings to the theme resource. ApplyFontSettings(dict); UpdateResourceDictionary(dict); @@ -152,11 +152,11 @@ namespace Flow.Launcher.Core.Resource 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 && @@ -172,7 +172,7 @@ namespace Flow.Launcher.Core.Resource 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) { @@ -197,7 +197,7 @@ namespace Flow.Launcher.Core.Resource // First, find the setters to remove and store them in a list var settersToRemove = style.Setters .OfType() - .Where(setter => + .Where(setter => setter.Property == Control.FontFamilyProperty || setter.Property == Control.FontStyleProperty || setter.Property == Control.FontWeightProperty || @@ -227,18 +227,18 @@ namespace Flow.Launcher.Core.Resource { var settersToRemove = style.Setters .OfType() - .Where(setter => + .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)); @@ -421,7 +421,7 @@ namespace Flow.Launcher.Core.Resource // Retrieve theme resource – always use the resource with font settings applied. var resourceDict = GetResourceDictionary(theme); - + UpdateResourceDictionary(resourceDict); _settings.Theme = theme; diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 18b206022..0e50420b0 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -43,6 +43,9 @@ MONITORINFOEXW WM_ENTERSIZEMOVE WM_EXITSIZEMOVE +OleInitialize +OleUninitialize + GetKeyboardLayout GetWindowThreadProcessId ActivateKeyboardLayout @@ -53,4 +56,4 @@ INPUTLANGCHANGE_FORWARD LOCALE_TRANSIENT_KEYBOARD1 LOCALE_TRANSIENT_KEYBOARD2 LOCALE_TRANSIENT_KEYBOARD3 -LOCALE_TRANSIENT_KEYBOARD4 \ No newline at end of file +LOCALE_TRANSIENT_KEYBOARD4 diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs index 71020369a..2d15b54c5 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs @@ -1,15 +1,15 @@ using System; using System.Text.Json.Serialization; +using System.Threading.Tasks; namespace Flow.Launcher.Infrastructure.UserSettings { + #region Base + public abstract class ShortcutBaseModel { public string Key { get; set; } - [JsonIgnore] - public Func Expand { get; set; } = () => { return ""; }; - public override bool Equals(object obj) { return obj is ShortcutBaseModel other && @@ -22,16 +22,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } - public class CustomShortcutModel : ShortcutBaseModel + public class BaseCustomShortcutModel : ShortcutBaseModel { public string Value { get; set; } - [JsonConstructorAttribute] - public CustomShortcutModel(string key, string value) + public BaseCustomShortcutModel(string key, string value) { Key = key; Value = value; - Expand = () => { return Value; }; } public void Deconstruct(out string key, out string value) @@ -40,26 +38,69 @@ namespace Flow.Launcher.Infrastructure.UserSettings value = Value; } - public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut) + public static implicit operator (string Key, string Value)(BaseCustomShortcutModel shortcut) { return (shortcut.Key, shortcut.Value); } - public static implicit operator CustomShortcutModel((string Key, string Value) shortcut) + public static implicit operator BaseCustomShortcutModel((string Key, string Value) shortcut) { - return new CustomShortcutModel(shortcut.Key, shortcut.Value); + return new BaseCustomShortcutModel(shortcut.Key, shortcut.Value); } } - public class BuiltinShortcutModel : ShortcutBaseModel + public class BaseBuiltinShortcutModel : ShortcutBaseModel { public string Description { get; set; } - public BuiltinShortcutModel(string key, string description, Func expand) + public BaseBuiltinShortcutModel(string key, string description) { Key = key; Description = description; - Expand = expand ?? (() => { return ""; }); } } + + #endregion + + #region Custom Shortcut + + public class CustomShortcutModel : BaseCustomShortcutModel + { + [JsonIgnore] + public Func Expand { get; set; } = () => { return string.Empty; }; + + [JsonConstructor] + public CustomShortcutModel(string key, string value) : base(key, value) + { + Expand = () => { return Value; }; + } + } + + #endregion + + #region Builtin Shortcut + + public class BuiltinShortcutModel : BaseBuiltinShortcutModel + { + [JsonIgnore] + public Func Expand { get; set; } = () => { return string.Empty; }; + + public BuiltinShortcutModel(string key, string description, Func expand) : base(key, description) + { + Expand = expand ?? (() => { return string.Empty; }); + } + } + + public class AsyncBuiltinShortcutModel : BaseBuiltinShortcutModel + { + [JsonIgnore] + public Func> ExpandAsync { get; set; } = () => { return Task.FromResult(string.Empty); }; + + public AsyncBuiltinShortcutModel(string key, string description, Func> expandAsync) : base(key, description) + { + ExpandAsync = expandAsync ?? (() => { return Task.FromResult(string.Empty); }); + } + } + + #endregion } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 1cccc38d9..b7a1d1f63 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -312,9 +312,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings public ObservableCollection CustomShortcuts { get; set; } = new ObservableCollection(); [JsonIgnore] - public ObservableCollection BuiltinShortcuts { get; set; } = new() + public ObservableCollection BuiltinShortcuts { get; set; } = new() { - new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), + new AsyncBuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText)), new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath) }; diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 2788060eb..783ade14e 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -5,6 +5,8 @@ using System.Diagnostics; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Interop; using System.Windows.Markup; @@ -337,6 +339,78 @@ namespace Flow.Launcher.Infrastructure #endregion + #region STA Thread + + /* + Inspired by https://github.com/files-community/Files code on STA Thread handling. + */ + + public static Task StartSTATaskAsync(Action action) + { + var taskCompletionSource = new TaskCompletionSource(); + Thread thread = new(() => + { + PInvoke.OleInitialize(); + + try + { + action(); + taskCompletionSource.SetResult(); + } + catch (System.Exception ex) + { + taskCompletionSource.SetException(ex); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return taskCompletionSource.Task; + } + + public static Task StartSTATaskAsync(Func func) + { + var taskCompletionSource = new TaskCompletionSource(); + + Thread thread = new(() => + { + PInvoke.OleInitialize(); + + try + { + taskCompletionSource.SetResult(func()); + } + catch (System.Exception ex) + { + taskCompletionSource.SetException(ex); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return taskCompletionSource.Task; + } + + #endregion + #region Keyboard Layout private const string UserProfileRegistryPath = @"Control Panel\International\User Profile"; diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 31580fbe8..d4eb02a90 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -471,8 +471,11 @@ namespace Flow.Launcher.Plugin public Task UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default); /// - /// Get the plugin manifest + /// Get the plugin manifest. /// + /// + /// If Flow cannot get manifest data, this could be null + /// /// public IReadOnlyList GetPluginManifest(); diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 913dc31ae..c3eede4c6 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -8,7 +8,8 @@ namespace Flow.Launcher.Plugin public class Query { /// - /// Raw query, this includes action keyword if it has + /// Raw query, this includes action keyword if it has. + /// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace. /// We didn't recommend use this property directly. You should always use Search property. /// public string RawQuery { get; internal init; } @@ -63,10 +64,10 @@ namespace Flow.Launcher.Plugin /// [JsonIgnore] public string FirstSearch => SplitSearch(0); - + [JsonIgnore] private string _secondToEndSearch; - + /// /// strings from second search (including) to last search /// diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 1b57d5cbe..402812a92 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -23,6 +23,7 @@ using Flow.Launcher.SettingPages.ViewModels; using Flow.Launcher.ViewModel; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.Threading; namespace Flow.Launcher { @@ -198,6 +199,10 @@ namespace Flow.Launcher Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; + // Initialize hotkey mapper instantly after main window is created because + // it will steal focus from main window which causes window hide + HotKeyMapper.Initialize(); + // Main windows needs initialized before theme change because of blur settings Ioc.Default.GetRequiredService().ChangeTheme(); @@ -239,6 +244,7 @@ namespace Flow.Launcher } } + [Conditional("RELEASE")] private void AutoUpdates() { _ = Task.Run(async () => @@ -296,13 +302,12 @@ namespace Flow.Launcher [Conditional("RELEASE")] private static void RegisterAppDomainExceptions() { - AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; + AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledException; } /// /// Let exception throw as normal is better for Debug /// - [Conditional("RELEASE")] private static void RegisterTaskSchedulerUnhandledException() { TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException; diff --git a/Flow.Launcher/Helper/DataWebRequestFactory.cs b/Flow.Launcher/Helper/DataWebRequestFactory.cs index 2e72ee240..db198ede4 100644 --- a/Flow.Launcher/Helper/DataWebRequestFactory.cs +++ b/Flow.Launcher/Helper/DataWebRequestFactory.cs @@ -28,18 +28,18 @@ public class DataWebRequestFactory : IWebRequestCreate public DataWebResponse(Uri uri) { - string uriString = uri.AbsoluteUri; + var uriString = uri.AbsoluteUri; - int commaIndex = uriString.IndexOf(','); - var headers = uriString.Substring(0, commaIndex).Split(';'); + var commaIndex = uriString.IndexOf(','); + var headers = uriString[..commaIndex].Split(';'); _contentType = headers[0]; - string dataString = uriString.Substring(commaIndex + 1); + var dataString = uriString[(commaIndex + 1)..]; _data = Convert.FromBase64String(dataString); } public override string ContentType { - get { return _contentType; } + get => _contentType; set { throw new NotSupportedException(); @@ -48,7 +48,7 @@ public class DataWebRequestFactory : IWebRequestCreate public override long ContentLength { - get { return _data.Length; } + get => _data.Length; set { throw new NotSupportedException(); diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs index b1ddba717..aa810ba65 100644 --- a/Flow.Launcher/Helper/ErrorReporting.cs +++ b/Flow.Launcher/Helper/ErrorReporting.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; @@ -10,33 +11,34 @@ namespace Flow.Launcher.Helper; public static class ErrorReporting { - private static void Report(Exception e) + private static void Report(Exception e, [CallerMemberName] string methodName = "UnHandledException") { - var logger = LogManager.GetLogger("UnHandledException"); + var logger = LogManager.GetLogger(methodName); logger.Fatal(ExceptionFormatter.FormatExcpetion(e)); var reportWindow = new ReportWindow(e); reportWindow.Show(); } - public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e) + public static void UnhandledException(object sender, UnhandledExceptionEventArgs e) { - //handle non-ui thread exceptions + // handle non-ui thread exceptions Report((Exception)e.ExceptionObject); } public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { - //handle ui thread exceptions + // handle ui thread exceptions Report(e.Exception); - //prevent application exist, so the user can copy prompted error info + // prevent application exist, so the user can copy prompted error info e.Handled = true; } public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { - //handle unobserved task exceptions + // handle unobserved task exceptions on UI thread Application.Current.Dispatcher.Invoke(() => Report(e.Exception)); - //prevent application exit, so the user can copy the prompted error info + // prevent application exit, so the user can copy the prompted error info + e.SetObserved(); } public static string RuntimeInfo() diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index d3252cb31..ca0ed33b5 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -394,6 +394,7 @@ This new Action Keyword is the same as old, please choose a different one Success Completed successfully + Failed to copy 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. diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 10aff050e..ae7b098a2 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -16,7 +16,6 @@ using System.Windows.Threading; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; -using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; @@ -182,9 +181,6 @@ namespace Flow.Launcher // Set the initial state of the QueryTextBoxCursorMovedToEnd property // Without this part, when shown for the first time, switching the context menu does not move the cursor to the end. _viewModel.QueryTextCursorMovedToEnd = false; - - // Initialize hotkey mapper after window is loaded - HotKeyMapper.Initialize(); // View model property changed event _viewModel.PropertyChanged += (o, e) => diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 3abc57b8a..5b8e8c9af 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -40,7 +40,7 @@ namespace Flow.Launcher private readonly Settings _settings; private readonly MainViewModel _mainVM; - // Must use getter to access Application.Current.Resources.MergedDictionaries so earlier + // Must use getter to avoid accessing Application.Current.Resources.MergedDictionaries so earlier in theme constructor private Theme _theme; private Theme Theme => _theme ??= Ioc.Default.GetRequiredService(); @@ -69,8 +69,7 @@ namespace Flow.Launcher _mainVM.ChangeQueryText(query, requery); } -#pragma warning disable VSTHRD100 // Avoid async void methods - + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] public async void RestartApp() { _mainVM.Hide(); @@ -89,8 +88,6 @@ namespace Flow.Launcher UpdateManager.RestartApp(Constant.ApplicationFileName); } -#pragma warning restore VSTHRD100 // Avoid async void methods - public void ShowMainWindow() => _mainVM.Show(); public void HideMainWindow() => _mainVM.Hide(); @@ -145,37 +142,92 @@ namespace Flow.Launcher ShellCommand.Execute(startInfo); } - public void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true) + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] + public async void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true) { if (string.IsNullOrEmpty(stringToCopy)) + { return; + } var isFile = File.Exists(stringToCopy); if (directCopy && (isFile || Directory.Exists(stringToCopy))) { - var paths = new StringCollection + // Sometimes the clipboard is locked and cannot be accessed, + // we need to retry a few times before giving up + var exception = await RetryActionOnSTAThreadAsync(() => + { + var paths = new StringCollection { stringToCopy }; - Clipboard.SetFileDropList(paths); - - if (showDefaultNotification) - ShowMsg( - $"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}", - GetTranslation("completedSuccessfully")); + Clipboard.SetFileDropList(paths); + }); + + if (exception == null) + { + if (showDefaultNotification) + { + ShowMsg( + $"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}", + GetTranslation("completedSuccessfully")); + } + } + else + { + LogException(nameof(PublicAPIInstance), "Failed to copy file/folder to clipboard", exception); + ShowMsgError(GetTranslation("failedToCopy")); + } } else { - Clipboard.SetDataObject(stringToCopy); + // Sometimes the clipboard is locked and cannot be accessed, + // we need to retry a few times before giving up + var exception = await RetryActionOnSTAThreadAsync(() => + { + // We should use SetText instead of SetDataObject to avoid the clipboard being locked by other applications + Clipboard.SetText(stringToCopy); + }); - if (showDefaultNotification) - ShowMsg( - $"{GetTranslation("copy")} {GetTranslation("textTitle")}", - GetTranslation("completedSuccessfully")); + if (exception == null) + { + if (showDefaultNotification) + { + ShowMsg( + $"{GetTranslation("copy")} {GetTranslation("textTitle")}", + GetTranslation("completedSuccessfully")); + } + } + else + { + LogException(nameof(PublicAPIInstance), "Failed to copy text to clipboard", exception); + ShowMsgError(GetTranslation("failedToCopy")); + } } } + private static async Task RetryActionOnSTAThreadAsync(Action action, int retryCount = 6, int retryDelay = 150) + { + for (var i = 0; i < retryCount; i++) + { + try + { + await Win32Helper.StartSTATaskAsync(action).ConfigureAwait(false); + break; + } + catch (Exception e) + { + if (i == retryCount - 1) + { + return e; + } + await Task.Delay(retryDelay); + } + } + return null; + } + public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible; public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs index 249e4dd42..07df0682d 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -79,8 +79,8 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel } } - public IList ExternalPlugins => - App.API.GetPluginManifest()?.Select(p => new PluginStoreItemViewModel(p)) + public IList ExternalPlugins => App.API.GetPluginManifest()? + .Select(p => new PluginStoreItemViewModel(p)) .OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease) .ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated) .ThenByDescending(p => p.Category == PluginStoreItemViewModel.None) diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs index de7cf15c3..abb314355 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs @@ -86,12 +86,22 @@ public partial class SettingsPanePluginsViewModel : BaseModel UpdateEnumDropdownLocalizations(); } - public string FilterText { get; set; } = string.Empty; + private string filterText = string.Empty; + public string FilterText + { + get => filterText; + set + { + if (filterText != value) + { + filterText = value; + OnPropertyChanged(); + } + } + } - public PluginViewModel? SelectedPlugin { get; set; } - - private IEnumerable? _pluginViewModels; - private IEnumerable PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins + private IList? _pluginViewModels; + public IList PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins .OrderBy(plugin => plugin.Metadata.Disabled) .ThenBy(plugin => plugin.Metadata.Name) .Select(plugin => new PluginViewModel @@ -102,13 +112,12 @@ public partial class SettingsPanePluginsViewModel : BaseModel .Where(plugin => plugin.PluginSettingsObject != null) .ToList(); - public List FilteredPluginViewModels => PluginViewModels - .Where(v => - string.IsNullOrEmpty(FilterText) || - App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() || - App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet() - ) - .ToList(); + public bool SatisfiesFilter(PluginViewModel plugin) + { + return string.IsNullOrEmpty(FilterText) || + App.API.FuzzySearch(FilterText, plugin.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() || + App.API.FuzzySearch(FilterText, plugin.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet(); + } [RelayCommand] private async Task OpenHelperAsync(Button button) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml index bb742f800..52d77f914 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml @@ -15,6 +15,12 @@ FocusManager.FocusedElement="{Binding ElementName=PluginFilterTextbox}" KeyDown="SettingsPanePlugins_OnKeyDown" mc:Ignorable="d"> + + + @@ -115,10 +121,9 @@ Background="{DynamicResource Color01B}" FontSize="14" ItemContainerStyle="{StaticResource PluginList}" - ItemsSource="{Binding FilteredPluginViewModels}" + ItemsSource="{Binding Source={StaticResource PluginCollectionView}}" ScrollViewer.CanContentScroll="False" ScrollViewer.HorizontalScrollBarVisibility="Disabled" - SelectedItem="{Binding SelectedPlugin}" SnapsToDevicePixels="True" Style="{DynamicResource PluginListStyle}" VirtualizingPanel.ScrollUnit="Pixel" diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs index 97574b3ce..e9490804a 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs @@ -1,7 +1,10 @@ -using System.Windows.Input; +using System.ComponentModel; +using System.Windows.Data; +using System.Windows.Input; using System.Windows.Navigation; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.SettingPages.ViewModels; +using Flow.Launcher.ViewModel; namespace Flow.Launcher.SettingPages.Views; @@ -17,12 +20,38 @@ public partial class SettingsPanePlugins DataContext = _viewModel; InitializeComponent(); } + _viewModel.PropertyChanged += ViewModel_PropertyChanged; base.OnNavigatedTo(e); } + private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(SettingsPanePluginsViewModel.FilterText)) + { + ((CollectionViewSource)FindResource("PluginCollectionView")).View.Refresh(); + } + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + _viewModel.PropertyChanged -= ViewModel_PropertyChanged; + base.OnNavigatingFrom(e); + } + private void SettingsPanePlugins_OnKeyDown(object sender, KeyEventArgs e) { if (e.Key is not Key.F || Keyboard.Modifiers is not ModifierKeys.Control) return; PluginFilterTextbox.Focus(); } + + private void PluginCollectionView_OnFilter(object sender, FilterEventArgs e) + { + if (e.Item is not PluginViewModel plugin) + { + e.Accepted = false; + return; + } + + e.Accepted = _viewModel.SatisfiesFilter(plugin); + } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 00675149b..2f1ed0f51 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -43,8 +43,7 @@ namespace Flow.Launcher.ViewModel private readonly UserSelectedRecord _userSelectedRecord; private readonly TopMostRecord _topMostRecord; - private CancellationTokenSource _updateSource; - private CancellationToken _updateToken; + private CancellationTokenSource _updateSource; // Used to cancel old query flows private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -241,7 +240,7 @@ namespace Flow.Launcher.ViewModel return; } - var token = e.Token == default ? _updateToken : e.Token; + var token = e.Token == default ? _updateSource.Token : e.Token; // make a clone to avoid possible issue that plugin will also change the list and items when updating view model var resultsCopy = DeepCloneResults(e.Results, token); @@ -255,8 +254,11 @@ namespace Flow.Launcher.ViewModel } PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query); - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, - token))) + + if (token.IsCancellationRequested) return; + + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, + token))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } @@ -640,7 +642,31 @@ namespace Flow.Launcher.ViewModel /// Force query even when Query Text doesn't change public void ChangeQueryText(string queryText, bool isReQuery = false) { - _ = ChangeQueryTextAsync(queryText, isReQuery); + // Must check access so that we will not block the UI thread which causes window visibility issue + if (!Application.Current.Dispatcher.CheckAccess()) + { + Application.Current.Dispatcher.Invoke(() => ChangeQueryText(queryText, isReQuery)); + return; + } + + if (QueryText != queryText) + { + // Change query text first + QueryText = queryText; + // When we are changing query from codes, we should not delay the query + Query(false, isReQuery: false); + + // set to false so the subsequent set true triggers + // PropertyChanged and MoveQueryTextToEnd is called + QueryTextCursorMovedToEnd = false; + } + else if (isReQuery) + { + // When we are re-querying, we should not delay the query + Query(false, isReQuery: true); + } + + QueryTextCursorMovedToEnd = true; } /// @@ -648,10 +674,10 @@ namespace Flow.Launcher.ViewModel /// private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false) { - // Must check access so that we will not block the UI thread which cause window visibility issue + // Must check access so that we will not block the UI thread which causes window visibility issue if (!Application.Current.Dispatcher.CheckAccess()) { - await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryText(queryText, isReQuery)); + await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryTextAsync(queryText, isReQuery)); return; } @@ -1050,7 +1076,18 @@ namespace Flow.Launcher.ViewModel public void Query(bool searchDelay, bool isReQuery = false) { - _ = QueryAsync(searchDelay, isReQuery); + if (QueryResultsSelected()) + { + _ = QueryResultsAsync(searchDelay, isReQuery); + } + else if (ContextMenuSelected()) + { + QueryContextMenu(); + } + else if (HistorySelected()) + { + QueryHistory(); + } } private async Task QueryAsync(bool searchDelay, bool isReQuery = false) @@ -1160,20 +1197,45 @@ namespace Flow.Launcher.ViewModel { _updateSource?.Cancel(); - var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); + var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); - var plugins = PluginManager.ValidPluginsForQuery(query); - - if (query == null || plugins.Count == 0) // shortcut expanded + if (query == null) // shortcut expanded { - Results.Clear(); + // Hide and clear results again because running query may show and add some results Results.Visibility = Visibility.Collapsed; + Results.Clear(); + + // Reset plugin icon PluginIconPath = null; PluginIconSource = null; SearchIconVisibility = Visibility.Visible; + + // Hide progress bar again because running query may set this to visible + ProgressBarVisibility = Visibility.Hidden; return; } - else if (plugins.Count == 1) + + _updateSource = new CancellationTokenSource(); + + ProgressBarVisibility = Visibility.Hidden; + _isQueryRunning = true; + + // Switch to ThreadPool thread + await TaskScheduler.Default; + + if (_updateSource.Token.IsCancellationRequested) return; + + // Update the query's IsReQuery property to true if this is a re-query + query.IsReQuery = isReQuery; + + // handle the exclusiveness of plugin using action keyword + RemoveOldQueryResults(query); + + _lastQuery = query; + + var plugins = PluginManager.ValidPluginsForQuery(query); + + if (plugins.Count == 1) { PluginIconPath = plugins.Single().Metadata.IcoPath; PluginIconSource = await App.API.LoadImageAsync(PluginIconPath); @@ -1186,42 +1248,20 @@ namespace Flow.Launcher.ViewModel SearchIconVisibility = Visibility.Visible; } - _updateSource?.Dispose(); - - var currentUpdateSource = new CancellationTokenSource(); - _updateSource = currentUpdateSource; - _updateToken = _updateSource.Token; - - ProgressBarVisibility = Visibility.Hidden; - _isQueryRunning = true; - - // Switch to ThreadPool thread - await TaskScheduler.Default; - - if (_updateSource.Token.IsCancellationRequested) - return; - - // Update the query's IsReQuery property to true if this is a re-query - query.IsReQuery = isReQuery; - - // handle the exclusiveness of plugin using action keyword - RemoveOldQueryResults(query); - - _lastQuery = query; - - if (string.IsNullOrEmpty(query.ActionKeyword)) + // Do not wait for performance improvement + /*if (string.IsNullOrEmpty(query.ActionKeyword)) { // Wait 15 millisecond for query change in global query // if query changes, return so that it won't be calculated await Task.Delay(15, _updateSource.Token); if (_updateSource.Token.IsCancellationRequested) return; - } + }*/ _ = Task.Delay(200, _updateSource.Token).ContinueWith(_ => { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet - if (!_updateSource.Token.IsCancellationRequested && _isQueryRunning) + if (_isQueryRunning) { ProgressBarVisibility = Visibility.Visible; } @@ -1248,12 +1288,12 @@ namespace Flow.Launcher.ViewModel // nothing to do here } - if (_updateSource.Token.IsCancellationRequested) - return; + if (_updateSource.Token.IsCancellationRequested) return; // this should happen once after all queries are done so progress bar should continue // until the end of all querying _isQueryRunning = false; + if (!_updateSource.Token.IsCancellationRequested) { // update to hidden if this is still the current query @@ -1269,8 +1309,7 @@ namespace Flow.Launcher.ViewModel await Task.Delay(searchDelayTime, token); - if (token.IsCancellationRequested) - return; + if (token.IsCancellationRequested) return; } // Since it is wrapped within a ThreadPool Thread, the synchronous context is null @@ -1279,8 +1318,7 @@ namespace Flow.Launcher.ViewModel var results = await PluginManager.QueryForPluginAsync(plugin, query, token); - if (token.IsCancellationRequested) - return; + if (token.IsCancellationRequested) return; IReadOnlyList resultsCopy; if (results == null) @@ -1301,6 +1339,8 @@ namespace Flow.Launcher.ViewModel } } + if (token.IsCancellationRequested) return; + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, token, reSelect))) { @@ -1309,16 +1349,16 @@ namespace Flow.Launcher.ViewModel } } - private Query ConstructQuery(string queryText, IEnumerable customShortcuts, - IEnumerable builtInShortcuts) + private async Task ConstructQueryAsync(string queryText, IEnumerable customShortcuts, + IEnumerable builtInShortcuts) { if (string.IsNullOrWhiteSpace(queryText)) { return null; } - StringBuilder queryBuilder = new(queryText); - StringBuilder queryBuilderTmp = new(queryText); + var queryBuilder = new StringBuilder(queryText); + var queryBuilderTmp = new StringBuilder(queryText); // Sorting order is important here, the reason is for matching longest shortcut by default foreach (var shortcut in customShortcuts.OrderByDescending(x => x.Key.Length)) @@ -1331,36 +1371,56 @@ namespace Flow.Launcher.ViewModel queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand()); } - string customExpanded = queryBuilder.ToString(); + // Applying builtin shortcuts + await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp); - Application.Current.Dispatcher.Invoke(() => + return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); + } + + private async Task BuildQueryAsync(IEnumerable builtInShortcuts, + StringBuilder queryBuilder, StringBuilder queryBuilderTmp) + { + var customExpanded = queryBuilder.ToString(); + + var queryChanged = false; + + foreach (var shortcut in builtInShortcuts) { - foreach (var shortcut in builtInShortcuts) + try { - try + if (customExpanded.Contains(shortcut.Key)) { - if (customExpanded.Contains(shortcut.Key)) + string expansion; + if (shortcut is BuiltinShortcutModel syncShortcut) { - var expansion = shortcut.Expand(); - queryBuilder.Replace(shortcut.Key, expansion); - queryBuilderTmp.Replace(shortcut.Key, expansion); + expansion = syncShortcut.Expand(); } - } - catch (Exception e) - { - App.API.LogException(ClassName, - $"Error when expanding shortcut {shortcut.Key}", - e); + else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut) + { + expansion = await asyncShortcut.ExpandAsync(); + } + else + { + continue; + } + queryBuilder.Replace(shortcut.Key, expansion); + queryBuilderTmp.Replace(shortcut.Key, expansion); + queryChanged = true; } } - }); + catch (Exception e) + { + App.API.LogException(ClassName, $"Error when expanding shortcut {shortcut.Key}", e); + } + } - // show expanded builtin shortcuts - // use private field to avoid infinite recursion - _queryText = queryBuilderTmp.ToString(); - - var query = QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); - return query; + if (queryChanged) + { + // show expanded builtin shortcuts + // use private field to avoid infinite recursion + _queryText = queryBuilderTmp.ToString(); + OnPropertyChanged(nameof(QueryText)); + } } private void RemoveOldQueryResults(Query query) @@ -1489,6 +1549,9 @@ namespace Flow.Launcher.ViewModel public void Show() { + // When application is exiting, we should not show the main window + if (App.Exiting) return; + // When application is exiting, the Application.Current will be null Application.Current?.Dispatcher.Invoke(() => { From 62e1252ac4a01aff2f2244182092887373282f87 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 2 May 2025 14:37:10 +0800 Subject: [PATCH 03/47] Use BackToQueryResults for code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 2f1ed0f51..2526adef8 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1170,7 +1170,7 @@ namespace Flow.Launcher.ViewModel OriginQuery = new Query { RawQuery = h.Query }, Action = _ => { - SelectedResults = Results; + App.API.BackToQueryResults(); App.API.ChangeQuery(h.Query); return false; } @@ -1600,10 +1600,7 @@ namespace Flow.Launcher.ViewModel await CloseExternalPreviewAsync(); } - if (!QueryResultsSelected()) - { - SelectedResults = Results; - } + BackToQueryResults(); switch (Settings.LastQueryMode) { From b1197858de49abc6c4583085b6124c462f82cfd0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 2 May 2025 14:40:18 +0800 Subject: [PATCH 04/47] Do not query when back from the context menu --- Flow.Launcher/ViewModel/MainViewModel.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 2526adef8..f0513c7d9 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -34,6 +34,7 @@ namespace Flow.Launcher.ViewModel private bool _isQueryRunning; private Query _lastQuery; private string _queryTextBeforeLeaveResults; + private string _ignoredQueryText = null; private readonly FlowLauncherJsonStorage _historyItemsStorage; private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; @@ -730,6 +731,9 @@ namespace Flow.Launcher.ViewModel if (isReturningFromContextMenu) { _queryText = _queryTextBeforeLeaveResults; + // When executing OnPropertyChanged, QueryTextBox_TextChanged1 and Query will be called + // So we need to ignore it so that we will not call Query again + _ignoredQueryText = _queryText; OnPropertyChanged(nameof(QueryText)); QueryTextCursorMovedToEnd = true; } @@ -1076,6 +1080,20 @@ namespace Flow.Launcher.ViewModel public void Query(bool searchDelay, bool isReQuery = false) { + if (_ignoredQueryText != null) + { + if (_ignoredQueryText == QueryText) + { + _ignoredQueryText = null; + return; + } + else + { + // If _ignoredQueryText does not match current QueryText, we should still execute Query + _ignoredQueryText = null; + } + } + if (QueryResultsSelected()) { _ = QueryResultsAsync(searchDelay, isReQuery); From f935d5f078dc414b72375cfa5d01e28e75033a24 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 2 May 2025 15:06:20 +0800 Subject: [PATCH 05/47] Improve code comments --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f0513c7d9..607b70fd2 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1432,10 +1432,10 @@ namespace Flow.Launcher.ViewModel } } + // Show expanded builtin shortcuts if (queryChanged) { - // show expanded builtin shortcuts - // use private field to avoid infinite recursion + // Use private field to avoid infinite recursion _queryText = queryBuilderTmp.ToString(); OnPropertyChanged(nameof(QueryText)); } From 247272c9aa7a986395a14beab3d6a5b2e22e4b75 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 2 May 2025 15:21:17 +0800 Subject: [PATCH 06/47] Do not query when expanding builtin shortcuts --- Flow.Launcher/ViewModel/MainViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 607b70fd2..c505c32a3 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1437,6 +1437,9 @@ namespace Flow.Launcher.ViewModel { // Use private field to avoid infinite recursion _queryText = queryBuilderTmp.ToString(); + // When executing OnPropertyChanged, QueryTextBox_TextChanged1 and Query will be called + // So we need to ignore it so that we will not call Query again + _ignoredQueryText = _queryText; OnPropertyChanged(nameof(QueryText)); } } From aa0f9b2e73c550964475f4b0e2454c3c5728ecb7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 08:58:40 +0800 Subject: [PATCH 07/47] Update name --- .../{IAsyncEmptyQuery.cs => IAsyncHomeQuery.cs} | 6 +++--- .../Interfaces/{IEmptyQuery.cs => IHomeQuery.cs} | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) rename Flow.Launcher.Plugin/Interfaces/{IAsyncEmptyQuery.cs => IAsyncHomeQuery.cs} (79%) rename Flow.Launcher.Plugin/Interfaces/{IEmptyQuery.cs => IHomeQuery.cs} (72%) diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncEmptyQuery.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs similarity index 79% rename from Flow.Launcher.Plugin/Interfaces/IAsyncEmptyQuery.cs rename to Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs index a18f0848d..f7a820f18 100644 --- a/Flow.Launcher.Plugin/Interfaces/IAsyncEmptyQuery.cs +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs @@ -7,17 +7,17 @@ namespace Flow.Launcher.Plugin /// /// Asynchronous Query Model for Flow Launcher When Query Text is Empty /// - public interface IAsyncEmptyQuery + public interface IAsyncHomeQuery { /// /// Asynchronous Querying When Query Text is Empty /// /// /// If the Querying method requires high IO transmission - /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncEmptyQuery interface + /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncHomeQuery interface /// /// Cancel when querying job is obsolete /// - Task> EmptyQueryAsync(CancellationToken token); + Task> HomeQueryAsync(CancellationToken token); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IEmptyQuery.cs b/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs similarity index 72% rename from Flow.Launcher.Plugin/Interfaces/IEmptyQuery.cs rename to Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs index 4ebdcf1fd..f3c9cfcad 100644 --- a/Flow.Launcher.Plugin/Interfaces/IEmptyQuery.cs +++ b/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs @@ -8,10 +8,10 @@ namespace Flow.Launcher.Plugin /// Synchronous Query Model for Flow Launcher When Query Text is Empty /// /// If the Querying method requires high IO transmission - /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncEmptyQuery interface + /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncHomeQuery interface /// /// - public interface IEmptyQuery : IAsyncEmptyQuery + public interface IHomeQuery : IAsyncHomeQuery { /// /// Querying When Query Text is Empty @@ -21,8 +21,8 @@ namespace Flow.Launcher.Plugin /// /// /// - List EmptyQuery(); + List HomeQuery(); - Task> IAsyncEmptyQuery.EmptyQueryAsync(CancellationToken token) => Task.Run(EmptyQuery); + Task> IAsyncHomeQuery.HomeQueryAsync(CancellationToken token) => Task.Run(HomeQuery); } } From 3089928599fca94487b21dbeff5fa35609e43463 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 16:31:49 +0800 Subject: [PATCH 08/47] Support home query interface --- .../Main.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Main.cs index 05e8d960f..48717816b 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Main.cs @@ -3,21 +3,33 @@ using System.Linq; namespace Flow.Launcher.Plugin.PluginIndicator { - public class Main : IPlugin, IPluginI18n + public class Main : IPlugin, IPluginI18n, IHomeQuery { internal PluginInitContext Context { get; private set; } public List Query(Query query) + { + return QueryResults(query); + } + + public List HomeQuery() + { + return QueryResults(); + } + + private List QueryResults(Query query = null) { var nonGlobalPlugins = GetNonGlobalPlugins(); + var querySearch = query?.Search ?? string.Empty; + var results = from keyword in nonGlobalPlugins.Keys let plugin = nonGlobalPlugins[keyword].Metadata - let keywordSearchResult = Context.API.FuzzySearch(query.Search, keyword) - let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : Context.API.FuzzySearch(query.Search, plugin.Name) + let keywordSearchResult = Context.API.FuzzySearch(querySearch, keyword) + let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : Context.API.FuzzySearch(querySearch, plugin.Name) let score = searchResult.Score where (searchResult.IsSearchPrecisionScoreMet() - || string.IsNullOrEmpty(query.Search)) // To list all available action keywords + || string.IsNullOrEmpty(querySearch)) // To list all available action keywords && !plugin.Disabled select new Result { From 0f09fea30b28526d72088e9c6603dfacf614e725 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 16:32:07 +0800 Subject: [PATCH 09/47] Make async home query to be feature interface --- Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs index f7a820f18..78d6454ae 100644 --- a/Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs @@ -7,7 +7,7 @@ namespace Flow.Launcher.Plugin /// /// Asynchronous Query Model for Flow Launcher When Query Text is Empty /// - public interface IAsyncHomeQuery + public interface IAsyncHomeQuery : IFeatures { /// /// Asynchronous Querying When Query Text is Empty From 17a0834bcda4ba856179543f152a04c987ff9c1c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 16:50:50 +0800 Subject: [PATCH 10/47] Support query for plugins with home query interface --- Flow.Launcher.Core/Plugin/PluginManager.cs | 51 ++++++++++++++ .../UserSettings/Settings.cs | 5 ++ Flow.Launcher.Plugin/Result.cs | 3 + Flow.Launcher/ViewModel/MainViewModel.cs | 68 ++++++++++++------- 4 files changed, 104 insertions(+), 23 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 72303c8b7..b0f896214 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -25,6 +25,7 @@ namespace Flow.Launcher.Core.Plugin private static readonly string ClassName = nameof(PluginManager); private static IEnumerable _contextMenuPlugins; + private static IEnumerable _homePlugins; public static List AllPlugins { get; private set; } public static readonly HashSet GlobalPlugins = new(); @@ -227,6 +228,8 @@ namespace Flow.Launcher.Core.Plugin await Task.WhenAll(InitTasks); _contextMenuPlugins = GetPluginsForInterface(); + _homePlugins = GetPluginsForInterface(); + foreach (var plugin in AllPlugins) { // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin @@ -274,6 +277,14 @@ namespace Flow.Launcher.Core.Plugin }; } + public static ICollection ValidPluginsForHomeQuery(Query query) + { + if (query is not null) + return Array.Empty(); + + return _homePlugins.ToList(); + } + public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) { var results = new List(); @@ -318,6 +329,36 @@ namespace Flow.Launcher.Core.Plugin return results; } + public static async Task> QueryHomeForPluginAsync(PluginPair pair, CancellationToken token) + { + var results = new List(); + var metadata = pair.Metadata; + + try + { + var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", + async () => results = await ((IAsyncHomeQuery)pair.Plugin).HomeQueryAsync(token).ConfigureAwait(false)); + + token.ThrowIfCancellationRequested(); + if (results == null) + return null; + UpdatePluginMetadata(results, metadata); + + token.ThrowIfCancellationRequested(); + } + catch (OperationCanceledException) + { + // null will be fine since the results will only be added into queue if the token hasn't been cancelled + return null; + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e); + return null; + } + return results; + } + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) @@ -333,6 +374,16 @@ namespace Flow.Launcher.Core.Plugin } } + private static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata) + { + foreach (var r in results) + { + r.PluginDirectory = metadata.PluginDirectory; + r.PluginID = metadata.ID; + r.OriginQuery = null; + } + } + /// /// get specified plugin, return null if not found /// diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index b7a1d1f63..2b5fc6bc0 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -158,6 +158,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } } + + public bool ShowHomeQuery { get; set; } = true; + public bool ShowHistoryRecordsForHomeQuery { get; set; } = false; + public int HistoryRecordsCountForHomeQuery { get; set; } = 5; + public int CustomExplorerIndex { get; set; } = 0; [JsonIgnore] diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index f0fcd48ff..060c03317 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -174,6 +174,9 @@ namespace Flow.Launcher.Plugin /// /// Query information associated with the result /// + /// + /// If the query is for home query, this will be null + /// internal Query OriginQuery { get; set; } /// diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 2f1ed0f51..b5afe4b7e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1198,21 +1198,31 @@ namespace Flow.Launcher.ViewModel _updateSource?.Cancel(); var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); + var homeQuery = query == null; + ICollection plugins = Array.Empty(); if (query == null) // shortcut expanded { - // Hide and clear results again because running query may show and add some results - Results.Visibility = Visibility.Collapsed; - Results.Clear(); + if (Settings.ShowHomeQuery) + { + plugins = PluginManager.ValidPluginsForHomeQuery(query); + } + + if (plugins.Count == 0) + { + // Hide and clear results again because running query may show and add some results + Results.Visibility = Visibility.Collapsed; + Results.Clear(); - // Reset plugin icon - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; + // Reset plugin icon + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; - // Hide progress bar again because running query may set this to visible - ProgressBarVisibility = Visibility.Hidden; - return; + // Hide progress bar again because running query may set this to visible + ProgressBarVisibility = Visibility.Hidden; + return; + } } _updateSource = new CancellationTokenSource(); @@ -1226,27 +1236,37 @@ namespace Flow.Launcher.ViewModel if (_updateSource.Token.IsCancellationRequested) return; // Update the query's IsReQuery property to true if this is a re-query - query.IsReQuery = isReQuery; + if (!homeQuery) query.IsReQuery = isReQuery; // handle the exclusiveness of plugin using action keyword RemoveOldQueryResults(query); _lastQuery = query; - var plugins = PluginManager.ValidPluginsForQuery(query); - - if (plugins.Count == 1) - { - PluginIconPath = plugins.Single().Metadata.IcoPath; - PluginIconSource = await App.API.LoadImageAsync(PluginIconPath); - SearchIconVisibility = Visibility.Hidden; - } - else + if (homeQuery) { + // Do not show plugin icon if this is a home query PluginIconPath = null; PluginIconSource = null; SearchIconVisibility = Visibility.Visible; } + else + { + plugins = PluginManager.ValidPluginsForQuery(query); + + if (plugins.Count == 1) + { + PluginIconPath = plugins.Single().Metadata.IcoPath; + PluginIconSource = await App.API.LoadImageAsync(PluginIconPath); + SearchIconVisibility = Visibility.Hidden; + } + else + { + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; + } + } // Do not wait for performance improvement /*if (string.IsNullOrEmpty(query.ActionKeyword)) @@ -1303,7 +1323,7 @@ namespace Flow.Launcher.ViewModel // Local function async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) { - if (searchDelay) + if (searchDelay && !homeQuery) // Do not delay for home query { var searchDelayTime = plugin.Metadata.SearchDelayTime ?? Settings.SearchDelayTime; @@ -1316,7 +1336,9 @@ namespace Flow.Launcher.ViewModel // Task.Yield will force it to run in ThreadPool await Task.Yield(); - var results = await PluginManager.QueryForPluginAsync(plugin, query, token); + var results = homeQuery ? + await PluginManager.QueryHomeForPluginAsync(plugin, token) : + await PluginManager.QueryForPluginAsync(plugin, query, token); if (token.IsCancellationRequested) return; @@ -1616,7 +1638,7 @@ namespace Flow.Launcher.ViewModel break; case LastQueryMode.ActionKeywordPreserved: case LastQueryMode.ActionKeywordSelected: - var newQuery = _lastQuery.ActionKeyword; + var newQuery = _lastQuery?.ActionKeyword; if (!string.IsNullOrEmpty(newQuery)) newQuery += " "; From 54994ddb7b264c51452e13dac7f7306b9fc44f41 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 17:02:39 +0800 Subject: [PATCH 11/47] Initialize home query when window is loaded --- Flow.Launcher/MainWindow.xaml.cs | 3 +++ Flow.Launcher/ViewModel/MainViewModel.cs | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index ae7b098a2..546f32cc5 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -292,6 +292,9 @@ namespace Flow.Launcher DependencyPropertyDescriptor .FromProperty(VisibilityProperty, typeof(StackPanel)) .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); + + // Initialize query state + _viewModel.InitializeQuery(); } private async void OnClosing(object sender, CancelEventArgs e) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index b5afe4b7e..8244d5765 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1074,6 +1074,14 @@ namespace Flow.Launcher.ViewModel #region Query + public void InitializeQuery() + { + if (Settings.ShowHomeQuery) + { + _ = QueryResultsAsync(false); + } + } + public void Query(bool searchDelay, bool isReQuery = false) { if (QueryResultsSelected()) From e9ef26a8dde26530f21e0ce244c0e03e9cf8d60b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 18:17:00 +0800 Subject: [PATCH 12/47] Change related setting names --- Flow.Launcher.Infrastructure/UserSettings/Settings.cs | 6 +++--- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 2b5fc6bc0..b630a4ecc 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -159,9 +159,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } - public bool ShowHomeQuery { get; set; } = true; - public bool ShowHistoryRecordsForHomeQuery { get; set; } = false; - public int HistoryRecordsCountForHomeQuery { get; set; } = 5; + public bool ShowHomePage { get; set; } = true; + public bool ShowHistoryResultsForHomePage { get; set; } = false; + public int MaxHistoryResultsToShowForHomePage { get; set; } = 5; public int CustomExplorerIndex { get; set; } = 0; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8244d5765..06cce3026 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1076,7 +1076,7 @@ namespace Flow.Launcher.ViewModel public void InitializeQuery() { - if (Settings.ShowHomeQuery) + if (Settings.ShowHomePage) { _ = QueryResultsAsync(false); } @@ -1211,7 +1211,7 @@ namespace Flow.Launcher.ViewModel if (query == null) // shortcut expanded { - if (Settings.ShowHomeQuery) + if (Settings.ShowHomePage) { plugins = PluginManager.ValidPluginsForHomeQuery(query); } From e8333331b0a348c9ba2eceb995943faea21130d0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 18:17:07 +0800 Subject: [PATCH 13/47] Add ui in general page --- Flow.Launcher/Languages/en.xaml | 4 ++ .../SettingsPaneGeneralViewModel.cs | 15 +++++- .../Views/SettingsPaneGeneral.xaml | 51 +++++++++++++++---- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index ca0ed33b5..af718946d 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -126,6 +126,10 @@ Open Use Previous Korean IME You can change the Previous Korean IME settings directly from here + Home Page + Show home page results when query text is empty. + Show History Results in Home Page + Maximum History Results Shown in Home Page Search Plugin diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index 1a887c4b7..840269b03 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -154,11 +154,22 @@ public partial class SettingsPaneGeneralViewModel : BaseModel { Settings.SearchDelayTime = value; OnPropertyChanged(); - OnPropertyChanged(nameof(SearchDelayTimeDisplay)); } } } - public string SearchDelayTimeDisplay => $"{SearchDelayTimeValue}ms"; + + public int MaxHistoryResultsToShowValue + { + get => Settings.MaxHistoryResultsToShowForHomePage; + set + { + if (Settings.MaxHistoryResultsToShowForHomePage != value) + { + Settings.MaxHistoryResultsToShowForHomePage = value; + OnPropertyChanged(); + } + } + } private void UpdateEnumDropdownLocalizations() { diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index d45d28d8b..c0c5613de 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -217,17 +217,46 @@ Title="{DynamicResource searchDelayTime}" Sub="{DynamicResource searchDelayTimeToolTip}" Type="InsideFit"> - - - + + + + + + + + + + + + + + From d6704ed5da47077b0751176d0e3185fb77ef320b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 22:09:25 +0800 Subject: [PATCH 14/47] Support history items --- Flow.Launcher/ViewModel/MainViewModel.cs | 81 ++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 06cce3026..d850e0f7c 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -50,6 +50,12 @@ namespace Flow.Launcher.ViewModel private readonly IReadOnlyList _emptyResult = new List(); + private readonly PluginMetadata _historyMetadata = new() + { + ID = "298303A65D128A845D28A7B83B3968C2", + Priority = 0 + }; + #endregion #region Constructor @@ -1300,11 +1306,30 @@ namespace Flow.Launcher.ViewModel // plugins are ICollection, meaning LINQ will get the Count and preallocate Array - var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch + Task[] tasks; + if (homeQuery) { - false => QueryTaskAsync(plugin, _updateSource.Token), - true => Task.CompletedTask - }).ToArray(); + var homeTasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch + { + false => QueryTaskAsync(plugin, _updateSource.Token), + true => Task.CompletedTask + }).ToList(); + + if (Settings.ShowHistoryResultsForHomePage) + { + homeTasks.Add(QueryHistoryTaskAsync()); + } + + tasks = homeTasks.ToArray(); + } + else + { + tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch + { + false => QueryTaskAsync(plugin, _updateSource.Token), + true => Task.CompletedTask + }).ToArray(); + } try { @@ -1377,6 +1402,54 @@ namespace Flow.Launcher.ViewModel App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } } + + async Task QueryHistoryTaskAsync() + { + // Since it is wrapped within a ThreadPool Thread, the synchronous context is null + // Task.Yield will force it to run in ThreadPool + await Task.Yield(); + + // Select last history results and revert its order to make sure last history results are on top + var lastHistoryResults = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); + + var historyResults = new List(); + foreach (var h in lastHistoryResults) + { + var title = App.API.GetTranslation("executeQuery"); + var time = App.API.GetTranslation("lastExecuteTime"); + var result = new Result + { + Title = string.Format(title, h.Query), + SubTitle = string.Format(time, h.ExecutedDateTime), + IcoPath = "Images\\history.png", + Preview = new Result.PreviewInfo + { + PreviewImagePath = Constant.HistoryIcon, + Description = string.Format(time, h.ExecutedDateTime) + }, + OriginQuery = new Query { RawQuery = h.Query }, + Action = _ => + { + SelectedResults = Results; + App.API.ChangeQuery(h.Query); + return false; + } + }; + historyResults.Add(result); + } + + // No need to make copy of results and update badge ico property + + if (_updateSource.Token.IsCancellationRequested) return; + + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(historyResults, _historyMetadata, query, + _updateSource.Token))) + { + App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); + } + + await Task.CompletedTask; + } } private async Task ConstructQueryAsync(string queryText, IEnumerable customShortcuts, From f2f4ebf0ac641eb1feafdb7f0f8feccabbd6d283 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 22:34:58 +0800 Subject: [PATCH 15/47] Support home state change & Add ui --- Flow.Launcher.Core/Plugin/PluginManager.cs | 6 +++ .../UserSettings/PluginSettings.cs | 3 ++ Flow.Launcher.Plugin/PluginMetadata.cs | 5 +++ Flow.Launcher/Languages/en.xaml | 6 +++ .../Controls/InstalledPluginDisplay.xaml | 11 +++++- .../SettingsPanePluginsViewModel.cs | 38 ++++++++++++++++++- Flow.Launcher/ViewModel/PluginViewModel.cs | 11 ++++++ 7 files changed, 78 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index b0f896214..caa3fd00b 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -221,6 +221,7 @@ namespace Flow.Launcher.Core.Plugin { API.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e); pair.Metadata.Disabled = true; + pair.Metadata.HomeDisabled = true; failedPlugins.Enqueue(pair); } })); @@ -429,6 +430,11 @@ namespace Flow.Launcher.Core.Plugin return results; } + public static bool IsHomePlugin(string id) + { + return _homePlugins.Any(p => p.Metadata.ID == id); + } + public static bool ActionKeywordRegistered(string actionKeyword) { // this method is only checking for action keywords (defined as not '*') registration diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index 7fb9b895a..837d0ebb6 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -67,6 +67,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings metadata.Disabled = settings.Disabled; metadata.Priority = settings.Priority; metadata.SearchDelayTime = settings.SearchDelayTime; + metadata.HomeDisabled = settings.HomeDiabled; } else { @@ -79,6 +80,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values ActionKeywords = metadata.ActionKeywords, // use default value Disabled = metadata.Disabled, + HomeDiabled = metadata.HomeDisabled, Priority = metadata.Priority, DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values SearchDelayTime = metadata.SearchDelayTime, // use default value @@ -128,5 +130,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// Used only to save the state of the plugin in settings /// public bool Disabled { get; set; } + public bool HomeDiabled { get; set; } } } diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index da10bc6a5..09803cbd7 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -50,6 +50,11 @@ namespace Flow.Launcher.Plugin /// public bool Disabled { get; set; } + /// + /// Whether plugin is disabled in home query. + /// + public bool HomeDisabled { get; set; } + /// /// Plugin execute file path. /// diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index af718946d..22ab2016c 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -130,6 +130,7 @@ Show home page results when query text is empty. Show History Results in Home Page Maximum History Results Shown in Home Page + This can only be edited if plugin supports Home feature and Home Page is enabled. Search Plugin @@ -152,6 +153,7 @@ Enabled Priority Search Delay + Home Page Current Priority New Priority Priority @@ -405,6 +407,10 @@ Search Delay Time Setting Input the search delay time in ms you like to use for the plugin. Input empty if you don't want to specify any, and the plugin will use default search delay time. + + Home Page + Enable the plugin home page state if you like to show the plugin results when query is empty. + Custom Query Hotkey Press a custom hotkey to open Flow Launcher and input the specified query automatically. diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml index 619da22dc..0842a64f3 100644 --- a/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml @@ -100,10 +100,19 @@ ToolTipService.InitialShowDelay="0" ToolTipService.ShowOnDisabled="True" Value="{Binding PluginSearchDelayTime, Mode=TwoWay}" /> - + + _isHomeOnOffSelected; + set + { + if (_isHomeOnOffSelected != value) + { + _isHomeOnOffSelected = value; + OnPropertyChanged(); + } + } + } + public SettingsPanePluginsViewModel(Settings settings) { _settings = settings; @@ -152,6 +166,18 @@ public partial class SettingsPanePluginsViewModel : BaseModel { Text = (string)Application.Current.Resources["searchDelayTimeTips"], TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = (string)Application.Current.Resources["homeTitle"], + FontSize = 18, + Margin = new Thickness(0, 24, 0, 10), + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = (string)Application.Current.Resources["homeTips"], + TextWrapping = TextWrapping.Wrap } } }, @@ -176,16 +202,25 @@ public partial class SettingsPanePluginsViewModel : BaseModel IsOnOffSelected = false; IsPrioritySelected = true; IsSearchDelaySelected = false; + IsHomeOnOffSelected = false; break; case DisplayMode.SearchDelay: IsOnOffSelected = false; IsPrioritySelected = false; IsSearchDelaySelected = true; + IsHomeOnOffSelected = false; + break; + case DisplayMode.HomeOnOff: + IsOnOffSelected = false; + IsPrioritySelected = false; + IsSearchDelaySelected = false; + IsHomeOnOffSelected = true; break; default: IsOnOffSelected = true; IsPrioritySelected = false; IsSearchDelaySelected = false; + IsHomeOnOffSelected = false; break; } } @@ -195,5 +230,6 @@ public enum DisplayMode { OnOff, Priority, - SearchDelay + SearchDelay, + HomeOnOff } diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 64a7f3db8..092896019 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -75,6 +75,16 @@ namespace Flow.Launcher.ViewModel } } + public bool PluginHomeState + { + get => !PluginPair.Metadata.HomeDisabled; + set + { + PluginPair.Metadata.HomeDisabled = !value; + PluginSettingsObject.HomeDiabled = !value; + } + } + public bool IsExpanded { get => _isExpanded; @@ -154,6 +164,7 @@ namespace Flow.Launcher.ViewModel public Infrastructure.UserSettings.Plugin PluginSettingsObject{ get; init; } public bool SearchDelayEnabled => Settings.SearchQueryResultsWithDelay; public string DefaultSearchDelay => Settings.SearchDelayTime.ToString(); + public bool HomeEnabled => Settings.ShowHomePage && PluginManager.IsHomePlugin(PluginPair.Metadata.ID); public void OnActionKeywordsTextChanged() { From 96bb62af27932df8e0afab3940ffc97cda28d349 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 3 May 2025 22:54:46 +0800 Subject: [PATCH 16/47] Refresh interface when Home Page is changed --- .../UserSettings/Settings.cs | 15 ++++++++++++++- Flow.Launcher/MainWindow.xaml.cs | 16 +++++++++++++++- Flow.Launcher/ViewModel/MainViewModel.cs | 7 ++----- Flow.Launcher/ViewModel/PluginViewModel.cs | 3 +++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index b630a4ecc..34bf4f90e 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -159,7 +159,20 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } - public bool ShowHomePage { get; set; } = true; + private bool _showHomePage { get; set; } = true; + public bool ShowHomePage + { + get => _showHomePage; + set + { + if (_showHomePage != value) + { + _showHomePage = value; + OnPropertyChanged(); + } + } + } + public bool ShowHistoryResultsForHomePage { get; set; } = false; public int MaxHistoryResultsToShowForHomePage { get; set; } = 5; diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 546f32cc5..6dcd8d22d 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -277,6 +277,17 @@ namespace Flow.Launcher case nameof(Settings.SettingWindowFont): InitializeContextMenu(); break; + case nameof(Settings.ShowHomePage): + // We should refresh results when these two settings are changed but we cannot do that + // Because the order of the results will change, making the interface look weird + // So we would better refresh results when query text is changed next time + /*case nameof(Settings.ShowHistoryResultsForHomePage): + case nameof(Settings.MaxHistoryResultsToShowForHomePage):*/ + if (string.IsNullOrEmpty(_viewModel.QueryText)) + { + _viewModel.QueryResults(); + } + break; } }; @@ -294,7 +305,10 @@ namespace Flow.Launcher .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); // Initialize query state - _viewModel.InitializeQuery(); + if (_settings.ShowHomePage && string.IsNullOrEmpty(_viewModel.QueryText)) + { + _viewModel.QueryResults(); + } } private async void OnClosing(object sender, CancelEventArgs e) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index d850e0f7c..993cc3ba9 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1080,12 +1080,9 @@ namespace Flow.Launcher.ViewModel #region Query - public void InitializeQuery() + public void QueryResults() { - if (Settings.ShowHomePage) - { - _ = QueryResultsAsync(false); - } + _ = QueryResultsAsync(false); } public void Query(bool searchDelay, bool isReQuery = false) diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 092896019..61f6dea33 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -82,6 +82,9 @@ namespace Flow.Launcher.ViewModel { PluginPair.Metadata.HomeDisabled = !value; PluginSettingsObject.HomeDiabled = !value; + // We should refresh results when these two settings are changed but we cannot do that + // Because the order of the results will change, making the interface look weird + // So we would better refresh results when query text is changed next time } } From 13cfbe54306d5eabe76d4f0e3f89c979e69a5f11 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 09:07:14 +0800 Subject: [PATCH 17/47] Check query results --- Flow.Launcher/MainWindow.xaml.cs | 7 +------ Flow.Launcher/ViewModel/PluginViewModel.cs | 3 --- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 6dcd8d22d..e2948c540 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -278,12 +278,7 @@ namespace Flow.Launcher InitializeContextMenu(); break; case nameof(Settings.ShowHomePage): - // We should refresh results when these two settings are changed but we cannot do that - // Because the order of the results will change, making the interface look weird - // So we would better refresh results when query text is changed next time - /*case nameof(Settings.ShowHistoryResultsForHomePage): - case nameof(Settings.MaxHistoryResultsToShowForHomePage):*/ - if (string.IsNullOrEmpty(_viewModel.QueryText)) + if (_viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText)) { _viewModel.QueryResults(); } diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 61f6dea33..092896019 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -82,9 +82,6 @@ namespace Flow.Launcher.ViewModel { PluginPair.Metadata.HomeDisabled = !value; PluginSettingsObject.HomeDiabled = !value; - // We should refresh results when these two settings are changed but we cannot do that - // Because the order of the results will change, making the interface look weird - // So we would better refresh results when query text is changed next time } } From 4500f1deebccad17076628174050a71e95881efa Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 09:14:57 +0800 Subject: [PATCH 18/47] Fix history items context menu issue --- Flow.Launcher/ViewModel/MainViewModel.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 993cc3ba9..8e708fbc4 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1127,9 +1127,20 @@ namespace Flow.Launcher.ViewModel if (selected != null) // SelectedItem returns null if selection is empty. { - var results = PluginManager.GetContextMenusForPlugin(selected); - results.Add(ContextMenuTopMost(selected)); - results.Add(ContextMenuPluginInfo(selected.PluginID)); + List results; + if (selected.PluginID == null) // SelectedItem from history in home page. + { + results = new() + { + ContextMenuTopMost(selected) + }; + } + else + { + results = PluginManager.GetContextMenusForPlugin(selected); + results.Add(ContextMenuTopMost(selected)); + results.Add(ContextMenuPluginInfo(selected.PluginID)); + } if (!string.IsNullOrEmpty(query)) { From 19d70592a8f66f5a4f16f3f840019c232865c1a3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 09:38:35 +0800 Subject: [PATCH 19/47] Add empty & global query for home page --- Flow.Launcher.Core/Plugin/PluginManager.cs | 5 +-- Flow.Launcher.Core/Plugin/QueryBuilder.cs | 21 +++++++-- Flow.Launcher/ViewModel/MainViewModel.cs | 51 ++++++++++------------ 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index caa3fd00b..c22651738 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -278,11 +278,8 @@ namespace Flow.Launcher.Core.Plugin }; } - public static ICollection ValidPluginsForHomeQuery(Query query) + public static ICollection ValidPluginsForHomeQuery() { - if (query is not null) - return Array.Empty(); - return _homePlugins.ToList(); } diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index 0ef3f30f5..fae821736 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -8,10 +8,23 @@ namespace Flow.Launcher.Core.Plugin { public static Query Build(string text, Dictionary nonGlobalPlugins) { + // home query + if (string.IsNullOrEmpty(text)) + { + return new Query() + { + Search = string.Empty, + RawQuery = string.Empty, + SearchTerms = Array.Empty(), + ActionKeyword = string.Empty + }; + } + // replace multiple white spaces with one white space var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries); if (terms.Length == 0) - { // nothing was typed + { + // nothing was typed return null; } @@ -21,13 +34,15 @@ namespace Flow.Launcher.Core.Plugin string[] searchTerms; if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled) - { // use non global plugin for query + { + // use non global plugin for query actionKeyword = possibleActionKeyword; search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty; searchTerms = terms[1..]; } else - { // non action keyword + { + // non action keyword actionKeyword = string.Empty; search = rawQuery.TrimStart(); searchTerms = terms; diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8e708fbc4..517cf5323 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1220,31 +1220,21 @@ namespace Flow.Launcher.ViewModel _updateSource?.Cancel(); var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); - var homeQuery = query == null; - ICollection plugins = Array.Empty(); if (query == null) // shortcut expanded { - if (Settings.ShowHomePage) - { - plugins = PluginManager.ValidPluginsForHomeQuery(query); - } - - if (plugins.Count == 0) - { - // Hide and clear results again because running query may show and add some results - Results.Visibility = Visibility.Collapsed; - Results.Clear(); + // Hide and clear results again because running query may show and add some results + Results.Visibility = Visibility.Collapsed; + Results.Clear(); - // Reset plugin icon - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; + // Reset plugin icon + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; - // Hide progress bar again because running query may set this to visible - ProgressBarVisibility = Visibility.Hidden; - return; - } + // Hide progress bar again because running query may set this to visible + ProgressBarVisibility = Visibility.Hidden; + return; } _updateSource = new CancellationTokenSource(); @@ -1258,16 +1248,22 @@ namespace Flow.Launcher.ViewModel if (_updateSource.Token.IsCancellationRequested) return; // Update the query's IsReQuery property to true if this is a re-query - if (!homeQuery) query.IsReQuery = isReQuery; + query.IsReQuery = isReQuery; // handle the exclusiveness of plugin using action keyword RemoveOldQueryResults(query); _lastQuery = query; + var homeQuery = query.RawQuery == string.Empty; + ICollection plugins = Array.Empty(); if (homeQuery) { - // Do not show plugin icon if this is a home query + if (Settings.ShowHomePage) + { + plugins = PluginManager.ValidPluginsForHomeQuery(); + } + PluginIconPath = null; PluginIconSource = null; SearchIconVisibility = Visibility.Visible; @@ -1317,18 +1313,17 @@ namespace Flow.Launcher.ViewModel Task[] tasks; if (homeQuery) { - var homeTasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch + tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch { false => QueryTaskAsync(plugin, _updateSource.Token), true => Task.CompletedTask - }).ToList(); + }).ToArray(); + // Query history results for home page firstly so it will be put on top of the results if (Settings.ShowHistoryResultsForHomePage) { - homeTasks.Add(QueryHistoryTaskAsync()); + await QueryHistoryTaskAsync(); } - - tasks = homeTasks.ToArray(); } else { @@ -1465,7 +1460,7 @@ namespace Flow.Launcher.ViewModel { if (string.IsNullOrWhiteSpace(queryText)) { - return null; + return QueryBuilder.Build(string.Empty, PluginManager.NonGlobalPlugins); } var queryBuilder = new StringBuilder(queryText); From 4ed1dc3bc12dc8eef98cc111e1013295715859e3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 10:28:20 +0800 Subject: [PATCH 20/47] Fix topmost issue in home page --- Flow.Launcher.Core/Plugin/PluginManager.cs | 14 ++------------ Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index c22651738..a3e80a73f 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -327,7 +327,7 @@ namespace Flow.Launcher.Core.Plugin return results; } - public static async Task> QueryHomeForPluginAsync(PluginPair pair, CancellationToken token) + public static async Task> QueryHomeForPluginAsync(PluginPair pair, Query query, CancellationToken token) { var results = new List(); var metadata = pair.Metadata; @@ -340,7 +340,7 @@ namespace Flow.Launcher.Core.Plugin token.ThrowIfCancellationRequested(); if (results == null) return null; - UpdatePluginMetadata(results, metadata); + UpdatePluginMetadata(results, metadata, query); token.ThrowIfCancellationRequested(); } @@ -372,16 +372,6 @@ namespace Flow.Launcher.Core.Plugin } } - private static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata) - { - foreach (var r in results) - { - r.PluginDirectory = metadata.PluginDirectory; - r.PluginID = metadata.ID; - r.OriginQuery = null; - } - } - /// /// get specified plugin, return null if not found /// diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 517cf5323..eb22dc3e2 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1373,7 +1373,7 @@ namespace Flow.Launcher.ViewModel await Task.Yield(); var results = homeQuery ? - await PluginManager.QueryHomeForPluginAsync(plugin, token) : + await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : await PluginManager.QueryForPluginAsync(plugin, query, token); if (token.IsCancellationRequested) return; From 35e4bfc937158f5a180188626fdd351daf3f188b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 11:05:23 +0800 Subject: [PATCH 21/47] Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index eb22dc3e2..64284d766 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1413,10 +1413,10 @@ namespace Flow.Launcher.ViewModel await Task.Yield(); // Select last history results and revert its order to make sure last history results are on top - var lastHistoryResults = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); + var historyResults = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); - var historyResults = new List(); - foreach (var h in lastHistoryResults) + var results = new List(); + foreach (var h in historyResults) { var title = App.API.GetTranslation("executeQuery"); var time = App.API.GetTranslation("lastExecuteTime"); @@ -1438,14 +1438,12 @@ namespace Flow.Launcher.ViewModel return false; } }; - historyResults.Add(result); + results.Add(result); } - // No need to make copy of results and update badge ico property - if (_updateSource.Token.IsCancellationRequested) return; - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(historyResults, _historyMetadata, query, + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, _historyMetadata, query, _updateSource.Token))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); From 2ab007b3b6db29ad4347a3f6b84e4f2459cecabc Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 11:26:00 +0800 Subject: [PATCH 22/47] Fix typos --- Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs | 6 +++--- Flow.Launcher/ViewModel/PluginViewModel.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index 837d0ebb6..920abc284 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -67,7 +67,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings metadata.Disabled = settings.Disabled; metadata.Priority = settings.Priority; metadata.SearchDelayTime = settings.SearchDelayTime; - metadata.HomeDisabled = settings.HomeDiabled; + metadata.HomeDisabled = settings.HomeDisabled; } else { @@ -80,7 +80,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values ActionKeywords = metadata.ActionKeywords, // use default value Disabled = metadata.Disabled, - HomeDiabled = metadata.HomeDisabled, + HomeDisabled = metadata.HomeDisabled, Priority = metadata.Priority, DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values SearchDelayTime = metadata.SearchDelayTime, // use default value @@ -130,6 +130,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// Used only to save the state of the plugin in settings /// public bool Disabled { get; set; } - public bool HomeDiabled { get; set; } + public bool HomeDisabled { get; set; } } } diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 092896019..01fa3d203 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -81,7 +81,7 @@ namespace Flow.Launcher.ViewModel set { PluginPair.Metadata.HomeDisabled = !value; - PluginSettingsObject.HomeDiabled = !value; + PluginSettingsObject.HomeDisabled = !value; } } From eaea38c13ab7c6536ee3b92bf8ad9b8ff436c599 Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sun, 4 May 2025 11:40:45 +0800 Subject: [PATCH 23/47] Fix typos Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs b/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs index f3c9cfcad..129f43b85 100644 --- a/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs +++ b/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs @@ -8,7 +8,7 @@ namespace Flow.Launcher.Plugin /// Synchronous Query Model for Flow Launcher When Query Text is Empty /// /// If the Querying method requires high IO transmission - /// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncHomeQuery interface + /// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncHomeQuery interface /// /// public interface IHomeQuery : IAsyncHomeQuery From 78f606f2fcb74467962bb582cd70f65c2d35104e Mon Sep 17 00:00:00 2001 From: Jack Ye <1160210343@qq.com> Date: Sun, 4 May 2025 11:40:57 +0800 Subject: [PATCH 24/47] Fix typos Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs b/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs index 129f43b85..81186fca2 100644 --- a/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs +++ b/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs @@ -17,7 +17,7 @@ namespace Flow.Launcher.Plugin /// Querying When Query Text is Empty /// /// This method will be called within a Task.Run, - /// so please avoid synchrously wait for long. + /// so please avoid synchronously wait for long. /// /// /// From 72bd1e60dbe7bdef77704e88641b818d1bb8f98f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 11:44:41 +0800 Subject: [PATCH 25/47] Remove code comments with issues --- Flow.Launcher.Plugin/Result.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 060c03317..f0fcd48ff 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -174,9 +174,6 @@ namespace Flow.Launcher.Plugin /// /// Query information associated with the result /// - /// - /// If the query is for home query, this will be null - /// internal Query OriginQuery { get; set; } /// From 0d9fb29f12948253a956f80fedae0d3a9473091e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 16:37:15 +0800 Subject: [PATCH 26/47] Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 64 +++++++++--------------- 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 6d7b4a8a4..f79596c96 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1192,8 +1192,27 @@ namespace Flow.Launcher.ViewModel var query = QueryText.ToLower().Trim(); History.Clear(); + var results = GetHistoryItems(_history.Items); + + if (!string.IsNullOrEmpty(query)) + { + var filtered = results.Where + ( + r => App.API.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() || + App.API.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() + ).ToList(); + History.AddResults(filtered, id); + } + else + { + History.AddResults(results, id); + } + } + + private static List GetHistoryItems(IEnumerable historyItems) + { var results = new List(); - foreach (var h in _history.Items) + foreach (var h in historyItems) { var title = App.API.GetTranslation("executeQuery"); var time = App.API.GetTranslation("lastExecuteTime"); @@ -1217,20 +1236,7 @@ namespace Flow.Launcher.ViewModel }; results.Add(result); } - - if (!string.IsNullOrEmpty(query)) - { - var filtered = results.Where - ( - r => App.API.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() || - App.API.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() - ).ToList(); - History.AddResults(filtered, id); - } - else - { - History.AddResults(results, id); - } + return results; } private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) @@ -1431,33 +1437,9 @@ namespace Flow.Launcher.ViewModel await Task.Yield(); // Select last history results and revert its order to make sure last history results are on top - var historyResults = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); + var historyItems = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); - var results = new List(); - foreach (var h in historyResults) - { - var title = App.API.GetTranslation("executeQuery"); - var time = App.API.GetTranslation("lastExecuteTime"); - var result = new Result - { - Title = string.Format(title, h.Query), - SubTitle = string.Format(time, h.ExecutedDateTime), - IcoPath = "Images\\history.png", - Preview = new Result.PreviewInfo - { - PreviewImagePath = Constant.HistoryIcon, - Description = string.Format(time, h.ExecutedDateTime) - }, - OriginQuery = new Query { RawQuery = h.Query }, - Action = _ => - { - SelectedResults = Results; - App.API.ChangeQuery(h.Query); - return false; - } - }; - results.Add(result); - } + var results = GetHistoryItems(historyItems); if (_updateSource.Token.IsCancellationRequested) return; From 67facb8f18f4325def9572e37ccf7f25fbb7a8fa Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 16:44:05 +0800 Subject: [PATCH 27/47] Use non-async version --- Flow.Launcher/ViewModel/MainViewModel.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f79596c96..5fd8d395f 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1346,7 +1346,7 @@ namespace Flow.Launcher.ViewModel // Query history results for home page firstly so it will be put on top of the results if (Settings.ShowHistoryResultsForHomePage) { - await QueryHistoryTaskAsync(); + QueryHistoryTask(); } } else @@ -1430,12 +1430,8 @@ namespace Flow.Launcher.ViewModel } } - async Task QueryHistoryTaskAsync() + void QueryHistoryTask() { - // Since it is wrapped within a ThreadPool Thread, the synchronous context is null - // Task.Yield will force it to run in ThreadPool - await Task.Yield(); - // Select last history results and revert its order to make sure last history results are on top var historyItems = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); @@ -1448,8 +1444,6 @@ namespace Flow.Launcher.ViewModel { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } - - await Task.CompletedTask; } } From 21d6ec20d45597ad872341afbcb82ef85a85ee87 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 18:29:39 +0800 Subject: [PATCH 28/47] Use null to distinguish between home query and global query --- Flow.Launcher.Core/Plugin/QueryBuilder.cs | 3 ++- Flow.Launcher.Plugin/Query.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index fae821736..82745c239 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -16,7 +16,8 @@ namespace Flow.Launcher.Core.Plugin Search = string.Empty, RawQuery = string.Empty, SearchTerms = Array.Empty(), - ActionKeyword = string.Empty + // must use null because we need to distinguish between home query and global query + ActionKeyword = null }; } diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index c3eede4c6..7d98a4afe 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -53,6 +53,7 @@ namespace Flow.Launcher.Plugin /// /// The action keyword part of this query. /// For global plugins this value will be empty. + /// For home query this value will be null. /// public string ActionKeyword { get; init; } From 28b8cb6013fb56de9d9946acc6a300af1e0883cf Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 21:43:48 +0800 Subject: [PATCH 29/47] Improve distinguish between home query and global query --- Flow.Launcher.Core/Plugin/QueryBuilder.cs | 3 +-- Flow.Launcher.Plugin/Query.cs | 1 - Flow.Launcher/ViewModel/MainViewModel.cs | 17 +++++++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index 82745c239..fae821736 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -16,8 +16,7 @@ namespace Flow.Launcher.Core.Plugin Search = string.Empty, RawQuery = string.Empty, SearchTerms = Array.Empty(), - // must use null because we need to distinguish between home query and global query - ActionKeyword = null + ActionKeyword = string.Empty }; } diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 7d98a4afe..c3eede4c6 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -53,7 +53,6 @@ namespace Flow.Launcher.Plugin /// /// The action keyword part of this query. /// For global plugins this value will be empty. - /// For home query this value will be null. /// public string ActionKeyword { get; init; } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 5fd8d395f..30df8250c 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -33,6 +33,7 @@ namespace Flow.Launcher.ViewModel private bool _isQueryRunning; private Query _lastQuery; + private bool _lastHomeQuery; private string _queryTextBeforeLeaveResults; private string _ignoredQueryText = null; @@ -1261,6 +1262,8 @@ namespace Flow.Launcher.ViewModel return; } + var homeQuery = query.RawQuery == string.Empty; + _updateSource = new CancellationTokenSource(); ProgressBarVisibility = Visibility.Hidden; @@ -1275,11 +1278,11 @@ namespace Flow.Launcher.ViewModel query.IsReQuery = isReQuery; // handle the exclusiveness of plugin using action keyword - RemoveOldQueryResults(query); + RemoveOldQueryResults(query, homeQuery); _lastQuery = query; + _lastHomeQuery = homeQuery; - var homeQuery = query.RawQuery == string.Empty; ICollection plugins = Array.Empty(); if (homeQuery) { @@ -1524,9 +1527,15 @@ namespace Flow.Launcher.ViewModel } } - private void RemoveOldQueryResults(Query query) + private void RemoveOldQueryResults(Query query, bool homeQuery) { - if (_lastQuery?.ActionKeyword != query?.ActionKeyword) + // If last or current query is home query, we need to clear the results + if (_lastHomeQuery || homeQuery) + { + Results.Clear(); + } + // If last and current query are not home query, we need to check action keyword + else if (_lastQuery?.ActionKeyword != query?.ActionKeyword) { Results.Clear(); } From 51fb1515a0d88636d26bfbc5913a994de8871209 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 22:04:01 +0800 Subject: [PATCH 30/47] Add more debug log info for query --- Flow.Launcher/ViewModel/MainViewModel.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c505c32a3..c481be02e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1215,10 +1215,14 @@ namespace Flow.Launcher.ViewModel { _updateSource?.Cancel(); + App.API.LogDebug(ClassName, $"Start query with text: {QueryText}"); + var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); if (query == null) // shortcut expanded { + App.API.LogDebug(ClassName, $"Clear query results"); + // Hide and clear results again because running query may show and add some results Results.Visibility = Visibility.Collapsed; Results.Clear(); @@ -1233,6 +1237,8 @@ namespace Flow.Launcher.ViewModel return; } + App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>"); + _updateSource = new CancellationTokenSource(); ProgressBarVisibility = Visibility.Hidden; @@ -1253,6 +1259,9 @@ namespace Flow.Launcher.ViewModel var plugins = PluginManager.ValidPluginsForQuery(query); + var validPluginNames = plugins.Select(x => $"<{x.Metadata.Name}>"); + App.API.LogDebug(ClassName, $"Valid <{plugins.Count}> plugins: {string.Join(" ", validPluginNames)}"); + if (plugins.Count == 1) { PluginIconPath = plugins.Single().Metadata.IcoPath; @@ -1321,6 +1330,8 @@ namespace Flow.Launcher.ViewModel // Local function async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) { + App.API.LogDebug(ClassName, $"Wait for querying plugin <{plugin.Metadata.Name}>"); + if (searchDelay) { var searchDelayTime = plugin.Metadata.SearchDelayTime ?? Settings.SearchDelayTime; @@ -1359,6 +1370,8 @@ namespace Flow.Launcher.ViewModel if (token.IsCancellationRequested) return; + App.API.LogDebug(ClassName, $"Update results for plugin <{plugin.Metadata.Name}>"); + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, token, reSelect))) { @@ -1448,6 +1461,8 @@ namespace Flow.Launcher.ViewModel { if (_lastQuery?.ActionKeyword != query?.ActionKeyword) { + App.API.LogDebug(ClassName, $"Remove old results"); + Results.Clear(); } } From 197316397a1596a694cc445351724542b54b66bd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 4 May 2025 22:05:57 +0800 Subject: [PATCH 31/47] Improve log info --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c481be02e..228e66edb 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1215,7 +1215,7 @@ namespace Flow.Launcher.ViewModel { _updateSource?.Cancel(); - App.API.LogDebug(ClassName, $"Start query with text: {QueryText}"); + App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>"); var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); From b9aa5a88cf5350ca7165fa570de225428472bd3f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 May 2025 08:30:53 +0800 Subject: [PATCH 32/47] Change variable name for code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 566cb6814..476c10e74 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -33,7 +33,7 @@ namespace Flow.Launcher.ViewModel private bool _isQueryRunning; private Query _lastQuery; - private bool _lastHomeQuery; + private bool _lastIsHomeQuery; private string _queryTextBeforeLeaveResults; private string _ignoredQueryText = null; @@ -1268,7 +1268,7 @@ namespace Flow.Launcher.ViewModel App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>"); - var homeQuery = query.RawQuery == string.Empty; + var isHomeQuery = query.RawQuery == string.Empty; _updateSource = new CancellationTokenSource(); @@ -1284,13 +1284,13 @@ namespace Flow.Launcher.ViewModel query.IsReQuery = isReQuery; // handle the exclusiveness of plugin using action keyword - RemoveOldQueryResults(query, homeQuery); + RemoveOldQueryResults(query, isHomeQuery); _lastQuery = query; - _lastHomeQuery = homeQuery; + _lastIsHomeQuery = isHomeQuery; ICollection plugins = Array.Empty(); - if (homeQuery) + if (isHomeQuery) { if (Settings.ShowHomePage) { @@ -1347,7 +1347,7 @@ namespace Flow.Launcher.ViewModel // plugins are ICollection, meaning LINQ will get the Count and preallocate Array Task[] tasks; - if (homeQuery) + if (isHomeQuery) { tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch { @@ -1397,7 +1397,7 @@ namespace Flow.Launcher.ViewModel { App.API.LogDebug(ClassName, $"Wait for querying plugin <{plugin.Metadata.Name}>"); - if (searchDelay && !homeQuery) // Do not delay for home query + if (searchDelay && !isHomeQuery) // Do not delay for home query { var searchDelayTime = plugin.Metadata.SearchDelayTime ?? Settings.SearchDelayTime; @@ -1410,7 +1410,7 @@ namespace Flow.Launcher.ViewModel // Task.Yield will force it to run in ThreadPool await Task.Yield(); - var results = homeQuery ? + var results = isHomeQuery ? await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : await PluginManager.QueryForPluginAsync(plugin, query, token); @@ -1542,10 +1542,10 @@ namespace Flow.Launcher.ViewModel } } - private void RemoveOldQueryResults(Query query, bool homeQuery) + private void RemoveOldQueryResults(Query query, bool isHomeQuery) { // If last or current query is home query, we need to clear the results - if (_lastHomeQuery || homeQuery) + if (_lastIsHomeQuery || isHomeQuery) { Results.Clear(); } From 2713c5babfaaf1a7989043f04b42b34b712116ca Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 May 2025 08:35:20 +0800 Subject: [PATCH 33/47] Add code comments --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 476c10e74..23958ca70 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -54,8 +54,8 @@ namespace Flow.Launcher.ViewModel private readonly PluginMetadata _historyMetadata = new() { - ID = "298303A65D128A845D28A7B83B3968C2", - Priority = 0 + ID = "298303A65D128A845D28A7B83B3968C2", // ID is for ResultsForUpdate constructor + Priority = 0 // Priority is for calculating scores in UpdateResultView }; #endregion From 41211e8cd380df56a44dc9df996ac2833b431dc1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 May 2025 08:44:08 +0800 Subject: [PATCH 34/47] Improve code comments --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 23958ca70..dc35a6aa9 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -54,7 +54,7 @@ namespace Flow.Launcher.ViewModel private readonly PluginMetadata _historyMetadata = new() { - ID = "298303A65D128A845D28A7B83B3968C2", // ID is for ResultsForUpdate constructor + ID = "298303A65D128A845D28A7B83B3968C2", // ID is for identifying the update plugin in UpdateActionAsync Priority = 0 // Priority is for calculating scores in UpdateResultView }; From 16404bc2a780430028e3515f19f02b2568f57b5e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 May 2025 12:54:15 +0800 Subject: [PATCH 35/47] Improve log information --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index dc35a6aa9..25c39ac7a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1547,13 +1547,13 @@ namespace Flow.Launcher.ViewModel // If last or current query is home query, we need to clear the results if (_lastIsHomeQuery || isHomeQuery) { + App.API.LogDebug(ClassName, $"Remove old results"); Results.Clear(); } // If last and current query are not home query, we need to check action keyword else if (_lastQuery?.ActionKeyword != query?.ActionKeyword) { App.API.LogDebug(ClassName, $"Remove old results"); - Results.Clear(); } } From 36a4f4176778253f00c09a7675ec43b07953282a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 May 2025 17:32:43 +0800 Subject: [PATCH 36/47] Do not need to clear the result when last and current query are home query --- Flow.Launcher/ViewModel/MainViewModel.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 25c39ac7a..a8e431d99 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1544,8 +1544,13 @@ namespace Flow.Launcher.ViewModel private void RemoveOldQueryResults(Query query, bool isHomeQuery) { + // If last and current query are home query, we don't need to clear the results + if (_lastIsHomeQuery && isHomeQuery) + { + return; + } // If last or current query is home query, we need to clear the results - if (_lastIsHomeQuery || isHomeQuery) + else if (_lastIsHomeQuery || isHomeQuery) { App.API.LogDebug(ClassName, $"Remove old results"); Results.Clear(); From 0882378988b72615118150db4c011eafd59ceca9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 May 2025 18:53:40 +0800 Subject: [PATCH 37/47] Add Glyph for history items & topmost items --- Flow.Launcher/ViewModel/MainViewModel.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index a8e431d99..a98172f0a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1221,19 +1221,15 @@ namespace Flow.Launcher.ViewModel { Title = string.Format(title, h.Query), SubTitle = string.Format(time, h.ExecutedDateTime), - IcoPath = "Images\\history.png", - Preview = new Result.PreviewInfo - { - PreviewImagePath = Constant.HistoryIcon, - Description = string.Format(time, h.ExecutedDateTime) - }, + IcoPath = Constant.HistoryIcon, OriginQuery = new Query { RawQuery = h.Query }, Action = _ => { App.API.BackToQueryResults(); App.API.ChangeQuery(h.Query); return false; - } + }, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C") }; results.Add(result); } @@ -1579,7 +1575,8 @@ namespace Flow.Launcher.ViewModel App.API.ShowMsg(App.API.GetTranslation("success")); App.API.ReQuery(); return false; - } + }, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE74B") }; } else @@ -1588,7 +1585,6 @@ namespace Flow.Launcher.ViewModel { Title = App.API.GetTranslation("setAsTopMostInThisQuery"), IcoPath = "Images\\up.png", - Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xeac2"), PluginDirectory = Constant.ProgramDirectory, Action = _ => { @@ -1596,7 +1592,8 @@ namespace Flow.Launcher.ViewModel App.API.ShowMsg(App.API.GetTranslation("success")); App.API.ReQuery(); return false; - } + }, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE74A") }; } From 7083849d149f4262b478f29d5c0c6834c78fd3c6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 5 May 2025 19:11:46 +0800 Subject: [PATCH 38/47] Remove unused codes --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index a98172f0a..6c4236db9 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -790,8 +790,6 @@ namespace Flow.Launcher.ViewModel } } } - - _selectedResults.Visibility = Visibility.Visible; } } From b1a48e296a9d3ee22b09b98c894e35dc4a2a3bc4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 13:58:28 +0800 Subject: [PATCH 39/47] Improve code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 6c4236db9..8ec29c216 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -35,7 +35,7 @@ namespace Flow.Launcher.ViewModel private Query _lastQuery; private bool _lastIsHomeQuery; private string _queryTextBeforeLeaveResults; - private string _ignoredQueryText = null; + private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results private readonly FlowLauncherJsonStorage _historyItemsStorage; private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; @@ -67,6 +67,7 @@ namespace Flow.Launcher.ViewModel _queryTextBeforeLeaveResults = ""; _queryText = ""; _lastQuery = new Query(); + _ignoredQueryText = null; // null as invalid value Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => From 639a5aebe5a5a5feaf6f1fa10c4845d56218367d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 14:02:18 +0800 Subject: [PATCH 40/47] Dispose _updateSource when creating new one & Use _updateToken instead of _updateSource.Token --- Flow.Launcher/ViewModel/MainViewModel.cs | 30 ++++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 8ec29c216..175f4ff84 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -46,6 +46,7 @@ namespace Flow.Launcher.ViewModel private readonly TopMostRecord _topMostRecord; private CancellationTokenSource _updateSource; // Used to cancel old query flows + private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -68,6 +69,8 @@ namespace Flow.Launcher.ViewModel _queryText = ""; _lastQuery = new Query(); _ignoredQueryText = null; // null as invalid value + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => @@ -249,7 +252,7 @@ namespace Flow.Launcher.ViewModel return; } - var token = e.Token == default ? _updateSource.Token : e.Token; + var token = e.Token == default ? _updateToken : e.Token; // make a clone to avoid possible issue that plugin will also change the list and items when updating view model var resultsCopy = DeepCloneResults(e.Results, token); @@ -1265,7 +1268,9 @@ namespace Flow.Launcher.ViewModel var isHomeQuery = query.RawQuery == string.Empty; + _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; @@ -1273,7 +1278,7 @@ namespace Flow.Launcher.ViewModel // Switch to ThreadPool thread await TaskScheduler.Default; - if (_updateSource.Token.IsCancellationRequested) return; + if (_updateToken.IsCancellationRequested) return; // Update the query's IsReQuery property to true if this is a re-query query.IsReQuery = isReQuery; @@ -1322,12 +1327,11 @@ namespace Flow.Launcher.ViewModel { // Wait 15 millisecond for query change in global query // if query changes, return so that it won't be calculated - await Task.Delay(15, _updateSource.Token); - if (_updateSource.Token.IsCancellationRequested) - return; + await Task.Delay(15, _updateToken); + if (_updateToken.IsCancellationRequested) return; }*/ - _ = Task.Delay(200, _updateSource.Token).ContinueWith(_ => + _ = Task.Delay(200, _updateToken).ContinueWith(_ => { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet if (_isQueryRunning) @@ -1335,7 +1339,7 @@ namespace Flow.Launcher.ViewModel ProgressBarVisibility = Visibility.Visible; } }, - _updateSource.Token, + _updateToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); @@ -1346,7 +1350,7 @@ namespace Flow.Launcher.ViewModel { tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch { - false => QueryTaskAsync(plugin, _updateSource.Token), + false => QueryTaskAsync(plugin, _updateToken), true => Task.CompletedTask }).ToArray(); @@ -1360,7 +1364,7 @@ namespace Flow.Launcher.ViewModel { tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch { - false => QueryTaskAsync(plugin, _updateSource.Token), + false => QueryTaskAsync(plugin, _updateToken), true => Task.CompletedTask }).ToArray(); } @@ -1375,13 +1379,13 @@ namespace Flow.Launcher.ViewModel // nothing to do here } - if (_updateSource.Token.IsCancellationRequested) return; + if (_updateToken.IsCancellationRequested) return; // this should happen once after all queries are done so progress bar should continue // until the end of all querying _isQueryRunning = false; - if (!_updateSource.Token.IsCancellationRequested) + if (!_updateToken.IsCancellationRequested) { // update to hidden if this is still the current query ProgressBarVisibility = Visibility.Hidden; @@ -1448,12 +1452,12 @@ namespace Flow.Launcher.ViewModel var results = GetHistoryItems(historyItems); - if (_updateSource.Token.IsCancellationRequested) return; + if (_updateToken.IsCancellationRequested) return; App.API.LogDebug(ClassName, $"Update results for history"); if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, _historyMetadata, query, - _updateSource.Token))) + _updateToken))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } From 265fd9c868e881da4f61060bb40385268c8d6420 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 14:13:14 +0800 Subject: [PATCH 41/47] Add update source lock --- Flow.Launcher/ViewModel/MainViewModel.cs | 27 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 175f4ff84..afef8f64e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -47,6 +47,7 @@ namespace Flow.Launcher.ViewModel private CancellationTokenSource _updateSource; // Used to cancel old query flows private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token + private readonly object _updateSourceLock = new(); private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -69,8 +70,11 @@ namespace Flow.Launcher.ViewModel _queryText = ""; _lastQuery = new Query(); _ignoredQueryText = null; // null as invalid value - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; + lock (_updateSourceLock) + { + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; + } Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => @@ -1240,7 +1244,10 @@ namespace Flow.Launcher.ViewModel private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { - _updateSource?.Cancel(); + lock (_updateSourceLock) + { + _updateSource.Cancel(); + } App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>"); @@ -1268,9 +1275,12 @@ namespace Flow.Launcher.ViewModel var isHomeQuery = query.RawQuery == string.Empty; - _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; + lock (_updateSourceLock) + { + _updateSource.Dispose(); // Dispose old update source to fix possible cancellation issue + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; + } ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; @@ -1888,7 +1898,10 @@ namespace Flow.Launcher.ViewModel { if (disposing) { - _updateSource?.Dispose(); + lock (_updateSourceLock) + { + _updateSource?.Dispose(); + } _resultsUpdateChannelWriter?.Complete(); if (_resultsViewUpdateTask?.IsCompleted == true) { From b156afed0bdac5cbe19749fcad2687d4bf2b9e20 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 14:14:10 +0800 Subject: [PATCH 42/47] Revert "Add update source lock" This reverts commit 265fd9c868e881da4f61060bb40385268c8d6420. --- Flow.Launcher/ViewModel/MainViewModel.cs | 27 ++++++------------------ 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index afef8f64e..175f4ff84 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -47,7 +47,6 @@ namespace Flow.Launcher.ViewModel private CancellationTokenSource _updateSource; // Used to cancel old query flows private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token - private readonly object _updateSourceLock = new(); private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; @@ -70,11 +69,8 @@ namespace Flow.Launcher.ViewModel _queryText = ""; _lastQuery = new Query(); _ignoredQueryText = null; // null as invalid value - lock (_updateSourceLock) - { - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; - } + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => @@ -1244,10 +1240,7 @@ namespace Flow.Launcher.ViewModel private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { - lock (_updateSourceLock) - { - _updateSource.Cancel(); - } + _updateSource?.Cancel(); App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>"); @@ -1275,12 +1268,9 @@ namespace Flow.Launcher.ViewModel var isHomeQuery = query.RawQuery == string.Empty; - lock (_updateSourceLock) - { - _updateSource.Dispose(); // Dispose old update source to fix possible cancellation issue - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; - } + _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue + _updateSource = new CancellationTokenSource(); + _updateToken = _updateSource.Token; ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; @@ -1898,10 +1888,7 @@ namespace Flow.Launcher.ViewModel { if (disposing) { - lock (_updateSourceLock) - { - _updateSource?.Dispose(); - } + _updateSource?.Dispose(); _resultsUpdateChannelWriter?.Complete(); if (_resultsViewUpdateTask?.IsCompleted == true) { From 2672512a62503a76d72777c3716cfe48dc1465b7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 14:14:50 +0800 Subject: [PATCH 43/47] Dispose the old CancellationTokenSource atomically to avoid races --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 175f4ff84..383256dcc 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1268,9 +1268,9 @@ namespace Flow.Launcher.ViewModel var isHomeQuery = query.RawQuery == string.Empty; - _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue - _updateSource = new CancellationTokenSource(); + var oldSource = Interlocked.Exchange(ref _updateSource, new CancellationTokenSource()); _updateToken = _updateSource.Token; + oldSource?.Dispose(); // Dispose old update source to fix possible cancellation issue ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; From 788cb3cc16cc639ffbe1d55acb3ab20f4f900396 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 19:17:33 +0800 Subject: [PATCH 44/47] Revert "Dispose the old CancellationTokenSource atomically to avoid races" This reverts commit 2672512a62503a76d72777c3716cfe48dc1465b7. --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 383256dcc..175f4ff84 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1268,9 +1268,9 @@ namespace Flow.Launcher.ViewModel var isHomeQuery = query.RawQuery == string.Empty; - var oldSource = Interlocked.Exchange(ref _updateSource, new CancellationTokenSource()); + _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue + _updateSource = new CancellationTokenSource(); _updateToken = _updateSource.Token; - oldSource?.Dispose(); // Dispose old update source to fix possible cancellation issue ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; From deb0c2139f6cbf6f6efd99ba640f65d76596edcd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 19:22:37 +0800 Subject: [PATCH 45/47] Remove unused initialization value --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 175f4ff84..aab2e2d03 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -69,8 +69,6 @@ namespace Flow.Launcher.ViewModel _queryText = ""; _lastQuery = new Query(); _ignoredQueryText = null; // null as invalid value - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; Settings = Ioc.Default.GetRequiredService(); Settings.PropertyChanged += (_, args) => From 297643c3e52d9800154390e40a5518e64cb86d9e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 6 May 2025 19:35:40 +0800 Subject: [PATCH 46/47] Revert all changes as master branch --- Flow.Launcher/ViewModel/MainViewModel.cs | 35 +++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index aab2e2d03..c0b74dc68 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1266,9 +1266,12 @@ namespace Flow.Launcher.ViewModel var isHomeQuery = query.RawQuery == string.Empty; - _updateSource?.Dispose(); // Dispose old update source to fix possible cancellation issue - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; + _updateSource?.Dispose(); + + var currentUpdateSource = new CancellationTokenSource(); + _updateSource = currentUpdateSource; + var currentCancellationToken = _updateSource.Token; + _updateToken = currentCancellationToken; ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; @@ -1276,7 +1279,7 @@ namespace Flow.Launcher.ViewModel // Switch to ThreadPool thread await TaskScheduler.Default; - if (_updateToken.IsCancellationRequested) return; + if (currentCancellationToken.IsCancellationRequested) return; // Update the query's IsReQuery property to true if this is a re-query query.IsReQuery = isReQuery; @@ -1325,11 +1328,11 @@ namespace Flow.Launcher.ViewModel { // Wait 15 millisecond for query change in global query // if query changes, return so that it won't be calculated - await Task.Delay(15, _updateToken); - if (_updateToken.IsCancellationRequested) return; + await Task.Delay(15, currentCancellationToken); + if (currentCancellationToken.IsCancellationRequested) return; }*/ - _ = Task.Delay(200, _updateToken).ContinueWith(_ => + _ = Task.Delay(200, currentCancellationToken).ContinueWith(_ => { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet if (_isQueryRunning) @@ -1337,7 +1340,7 @@ namespace Flow.Launcher.ViewModel ProgressBarVisibility = Visibility.Visible; } }, - _updateToken, + currentCancellationToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); @@ -1348,21 +1351,21 @@ namespace Flow.Launcher.ViewModel { tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch { - false => QueryTaskAsync(plugin, _updateToken), + false => QueryTaskAsync(plugin, currentCancellationToken), true => Task.CompletedTask }).ToArray(); // Query history results for home page firstly so it will be put on top of the results if (Settings.ShowHistoryResultsForHomePage) { - QueryHistoryTask(); + QueryHistoryTask(currentCancellationToken); } } else { tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch { - false => QueryTaskAsync(plugin, _updateToken), + false => QueryTaskAsync(plugin, currentCancellationToken), true => Task.CompletedTask }).ToArray(); } @@ -1377,13 +1380,13 @@ namespace Flow.Launcher.ViewModel // nothing to do here } - if (_updateToken.IsCancellationRequested) return; + if (currentCancellationToken.IsCancellationRequested) return; // this should happen once after all queries are done so progress bar should continue // until the end of all querying _isQueryRunning = false; - if (!_updateToken.IsCancellationRequested) + if (!currentCancellationToken.IsCancellationRequested) { // update to hidden if this is still the current query ProgressBarVisibility = Visibility.Hidden; @@ -1443,19 +1446,19 @@ namespace Flow.Launcher.ViewModel } } - void QueryHistoryTask() + void QueryHistoryTask(CancellationToken token) { // Select last history results and revert its order to make sure last history results are on top var historyItems = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); var results = GetHistoryItems(historyItems); - if (_updateToken.IsCancellationRequested) return; + if (token.IsCancellationRequested) return; App.API.LogDebug(ClassName, $"Update results for history"); if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, _historyMetadata, query, - _updateToken))) + token))) { App.API.LogError(ClassName, "Unable to add item to Result Update Queue"); } From 29f94d66c229cd4036fc00f48e64add87d0fab00 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 9 May 2025 15:57:18 +0800 Subject: [PATCH 47/47] Fix startup flicker --- Flow.Launcher/App.xaml.cs | 2 +- Flow.Launcher/MainWindow.xaml.cs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 402812a92..942e94470 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -203,7 +203,7 @@ namespace Flow.Launcher // it will steal focus from main window which causes window hide HotKeyMapper.Initialize(); - // Main windows needs initialized before theme change because of blur settings + // Initialize theme for main window Ioc.Default.GetRequiredService().ChangeTheme(); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index e2948c540..e243549e3 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -166,9 +166,6 @@ namespace Flow.Launcher // Force update position UpdatePosition(); - // Refresh frame - await _theme.RefreshFrameAsync(); - // Initialize resize mode after refreshing frame SetupResizeMode();