diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index eef4a0b710a..4f869901ca8 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -622,7 +622,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool remo API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"), string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); } - Settings.Plugins.Remove(plugin.ID); + Settings.RemovePluginSettings(plugin.ID); AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); } diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index a64457ffc53..495a4c1ab5f 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -50,7 +50,7 @@ public static List Plugins(List metadatas, PluginsSe return plugins; } - public static IEnumerable DotNetPlugins(List source) + private static IEnumerable DotNetPlugins(List source) { var erroredPlugins = new List(); @@ -132,7 +132,7 @@ public static IEnumerable DotNetPlugins(List source) return plugins; } - public static IEnumerable ExecutablePlugins(IEnumerable source) + private static IEnumerable ExecutablePlugins(IEnumerable source) { return source .Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase)) @@ -146,7 +146,7 @@ public static IEnumerable ExecutablePlugins(IEnumerable ExecutableV2Plugins(IEnumerable source) + private static IEnumerable ExecutableV2Plugins(IEnumerable source) { return source .Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase)) diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index 1d06e18f738..da92a358351 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings @@ -6,8 +7,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings public class PluginsSettings : BaseModel { private string pythonExecutablePath = string.Empty; - public string PythonExecutablePath { - get { return pythonExecutablePath; } + public string PythonExecutablePath + { + get => pythonExecutablePath; set { pythonExecutablePath = value; @@ -18,7 +20,7 @@ public string PythonExecutablePath { private string nodeExecutablePath = string.Empty; public string NodeExecutablePath { - get { return nodeExecutablePath; } + get => nodeExecutablePath; set { nodeExecutablePath = value; @@ -26,17 +28,32 @@ public string NodeExecutablePath } } - public Dictionary Plugins { get; set; } = new Dictionary(); + /// + /// Only used for serialization + /// + public Dictionary Plugins { get; set; } = new(); + /// + /// Update plugin settings with metadata. + /// FL will get default values from metadata first and then load settings to metadata + /// + /// Parsed plugin metadatas public void UpdatePluginSettings(List metadatas) { foreach (var metadata in metadatas) { if (Plugins.TryGetValue(metadata.ID, out var settings)) { + // If settings exist, update settings & metadata value + // update settings values with metadata if (string.IsNullOrEmpty(settings.Version)) + { settings.Version = metadata.Version; + } + settings.DefaultActionKeywords = metadata.ActionKeywords; // metadata provides default values + settings.DefaultSearchDelayTime = metadata.SearchDelayTime; // metadata provides default values + // update metadata values with settings if (settings.ActionKeywords?.Count > 0) { metadata.ActionKeywords = settings.ActionKeywords; @@ -49,30 +66,65 @@ public void UpdatePluginSettings(List metadatas) } metadata.Disabled = settings.Disabled; metadata.Priority = settings.Priority; + metadata.SearchDelayTime = settings.SearchDelayTime; } else { + // If settings does not exist, create a new one Plugins[metadata.ID] = new Plugin { ID = metadata.ID, Name = metadata.Name, Version = metadata.Version, - ActionKeywords = metadata.ActionKeywords, + DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values + ActionKeywords = metadata.ActionKeywords, // use default value Disabled = metadata.Disabled, - Priority = metadata.Priority + Priority = metadata.Priority, + DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values + SearchDelayTime = metadata.SearchDelayTime, // use default value }; } } } + + public Plugin GetPluginSettings(string id) + { + if (Plugins.TryGetValue(id, out var plugin)) + { + return plugin; + } + return null; + } + + public Plugin RemovePluginSettings(string id) + { + Plugins.Remove(id, out var plugin); + return plugin; + } } + public class Plugin { public string ID { get; set; } + public string Name { get; set; } + public string Version { get; set; } - public List ActionKeywords { get; set; } // a reference of the action keywords from plugin manager + + [JsonIgnore] + public List DefaultActionKeywords { get; set; } + + // a reference of the action keywords from plugin manager + public List ActionKeywords { get; set; } + public int Priority { get; set; } + [JsonIgnore] + public SearchDelayTime? DefaultSearchDelayTime { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public SearchDelayTime? SearchDelayTime { get; set; } + /// /// Used only to save the state of the plugin in settings /// diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index bd146f49a0b..6cb20d12fdd 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -112,7 +112,7 @@ public string Theme public double SettingWindowHeight { get; set; } = 700; public double? SettingWindowTop { get; set; } = null; public double? SettingWindowLeft { get; set; } = null; - public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal; + public WindowState SettingWindowState { get; set; } = WindowState.Normal; bool _showPlaceholder { get; set; } = false; public bool ShowPlaceholder @@ -310,7 +310,7 @@ public bool KeepMaxResults bool _hideNotifyIcon { get; set; } public bool HideNotifyIcon { - get { return _hideNotifyIcon; } + get => _hideNotifyIcon; set { _hideNotifyIcon = value; @@ -320,6 +320,11 @@ public bool HideNotifyIcon public bool LeaveCmdOpen { get; set; } public bool HideWhenDeactivated { get; set; } = true; + public bool SearchQueryResultsWithDelay { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public SearchDelayTime SearchDelayTime { get; set; } = SearchDelayTime.Normal; + [JsonConverter(typeof(JsonStringEnumConverter))] public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor; @@ -342,7 +347,6 @@ public bool HideNotifyIcon [JsonIgnore] public bool WMPInstalled { get; set; } = true; - // This needs to be loaded last by staying at the bottom public PluginsSettings PluginSettings { get; set; } = new PluginsSettings(); diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index eb276509b6d..1496765cea4 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -92,12 +92,18 @@ internal set /// All action keywords of plugin. /// public List ActionKeywords { get; set; } - + /// /// Hide plugin keyword setting panel. /// public bool HideActionKeywordPanel { get; set; } + /// + /// Plugin search delay time. Null means use default search delay time. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public SearchDelayTime? SearchDelayTime { get; set; } = null; + /// /// Plugin icon path. /// diff --git a/Flow.Launcher.Plugin/SearchDelayTime.cs b/Flow.Launcher.Plugin/SearchDelayTime.cs new file mode 100644 index 00000000000..ae1daabe08a --- /dev/null +++ b/Flow.Launcher.Plugin/SearchDelayTime.cs @@ -0,0 +1,32 @@ +namespace Flow.Launcher.Plugin; + +/// +/// Enum for search delay time +/// +public enum SearchDelayTime +{ + /// + /// Very long search delay time. 250ms. + /// + VeryLong, + + /// + /// Long search delay time. 200ms. + /// + Long, + + /// + /// Normal search delay time. 150ms. Default value. + /// + Normal, + + /// + /// Short search delay time. 100ms. + /// + Short, + + /// + /// Very short search delay time. 50ms. + /// + VeryShort +} diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 7c96fe88dfb..364be40096b 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -102,6 +102,15 @@ Always Preview Always open preview panel when Flow activates. Press {0} to toggle preview. Shadow effect is not allowed while current theme has blur effect enabled + Search Delay + Delay for a while to search when typing. This reduces interface jumpiness and result load. + Default Search Delay Time + Plugin default delay time after which search results appear when typing is stopped. + Very long + Long + Normal + Short + Very short Search Plugin @@ -118,6 +127,8 @@ Current action keyword New action keyword Change Action Keywords + Plugin seach delay time + Change Plugin Seach Delay Time Current Priority New Priority Priority @@ -133,6 +144,7 @@ Plugins: {0} - Fail to remove plugin settings files, please remove them manually Fail to remove plugin cache Plugins: {0} - Fail to remove plugin cache files, please remove them manually + Default Plugin Store @@ -353,6 +365,12 @@ Completed successfully 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. + + Search Delay Time Setting + Select the search delay time you like to use for the plugin. Select "{0}" if you don't want to specify any, and the plugin will use default search delay time. + Current search delay time + New search delay time + Custom Query Hotkey Press a custom hotkey to open Flow Launcher and input the specified query automatically. diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 82ac63b7da6..31bc2ba5046 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -251,7 +251,8 @@ PreviewDragOver="QueryTextBox_OnPreviewDragOver" PreviewKeyUp="QueryTextBox_KeyUp" Style="{DynamicResource QueryBoxStyle}" - Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" + Text="{Binding QueryText, Mode=OneWay}" + TextChanged="QueryTextBox_TextChanged1" Visibility="Visible" WindowChrome.IsHitTestVisibleInChrome="True"> diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 8f22d64b8d6..011d46d6b21 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -22,6 +22,8 @@ using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.ViewModel; using ModernWpf.Controls; +using DataObject = System.Windows.DataObject; +using Key = System.Windows.Input.Key; using MouseButtons = System.Windows.Forms.MouseButtons; using NotifyIcon = System.Windows.Forms.NotifyIcon; using Screen = System.Windows.Forms.Screen; @@ -165,6 +167,8 @@ private async void OnLoaded(object sender, RoutedEventArgs _) // 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; + + // View model property changed event _viewModel.PropertyChanged += (o, e) => { switch (e.PropertyName) @@ -227,6 +231,7 @@ private async void OnLoaded(object sender, RoutedEventArgs _) } }; + // Settings property changed event _settings.PropertyChanged += (o, e) => { switch (e.PropertyName) @@ -1050,7 +1055,7 @@ private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e) { e.Handled = true; } - + #endregion #region Placeholder @@ -1101,6 +1106,17 @@ private void SetupResizeMode() } } + #endregion + + #region Search Delay + + private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) + { + var textBox = (TextBox)sender; + _viewModel.QueryText = textBox.Text; + _viewModel.Query(_settings.SearchQueryResultsWithDelay); + } + #endregion #region IDisposable diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml index ed3c2969060..b19c668e0bd 100644 --- a/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml @@ -1,14 +1,16 @@ - + + Source="{Binding Image, Mode=OneWay, IsAsync=True}" /> - + - + @@ -95,10 +98,12 @@ + + + BorderThickness="0 1 0 0"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SearchDelayTimeWindow.xaml.cs b/Flow.Launcher/SearchDelayTimeWindow.xaml.cs new file mode 100644 index 00000000000..4a3c9f5a730 --- /dev/null +++ b/Flow.Launcher/SearchDelayTimeWindow.xaml.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Windows; +using Flow.Launcher.Plugin; +using Flow.Launcher.SettingPages.ViewModels; +using Flow.Launcher.ViewModel; +using static Flow.Launcher.SettingPages.ViewModels.SettingsPaneGeneralViewModel; + +namespace Flow.Launcher; + +public partial class SearchDelayTimeWindow : Window +{ + private readonly PluginViewModel _pluginViewModel; + + public SearchDelayTimeWindow(PluginViewModel pluginViewModel) + { + InitializeComponent(); + _pluginViewModel = pluginViewModel; + } + + private void SearchDelayTimeWindow_OnLoaded(object sender, RoutedEventArgs e) + { + tbSearchDelayTimeTips.Text = string.Format(App.API.GetTranslation("searchDelayTime_tips"), + App.API.GetTranslation("default")); + tbOldSearchDelayTime.Text = _pluginViewModel.SearchDelayTimeText; + var searchDelayTimes = DropdownDataGeneric.GetValues("SearchDelayTime"); + SearchDelayTimeData selected = null; + // Because default value is SearchDelayTime.VeryShort, we need to get selected value before adding default value + if (_pluginViewModel.PluginSearchDelayTime != null) + { + selected = searchDelayTimes.FirstOrDefault(x => x.Value == _pluginViewModel.PluginSearchDelayTime); + } + // Add default value to the beginning of the list + // When _pluginViewModel.PluginSearchDelayTime equals null, we will select this + searchDelayTimes.Insert(0, new SearchDelayTimeData { Display = App.API.GetTranslation("default"), LocalizationKey = "default" }); + selected ??= searchDelayTimes.FirstOrDefault(); + cbDelay.ItemsSource = searchDelayTimes; + cbDelay.SelectedItem = selected; + cbDelay.Focus(); + } + + private void BtnCancel_OnClick(object sender, RoutedEventArgs e) + { + Close(); + } + + private void btnDone_OnClick(object sender, RoutedEventArgs _) + { + // Update search delay time + var selected = cbDelay.SelectedItem as SearchDelayTimeData; + SearchDelayTime? changedValue = selected?.LocalizationKey != "default" ? selected.Value : null; + _pluginViewModel.PluginSearchDelayTime = changedValue; + + // Update search delay time text and close window + _pluginViewModel.OnSearchDelayTimeChanged(); + Close(); + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs b/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs index 15a81443645..c8c119e94bf 100644 --- a/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs +++ b/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Flow.Launcher.Core.Resource; using Flow.Launcher.Plugin; namespace Flow.Launcher.SettingPages.ViewModels; @@ -9,7 +8,7 @@ public class DropdownDataGeneric : BaseModel where TValue : Enum { public string Display { get; set; } public TValue Value { get; private init; } - private string LocalizationKey { get; init; } + public string LocalizationKey { get; set; } public static List GetValues(string keyPrefix) where TR : DropdownDataGeneric, new() { @@ -19,7 +18,7 @@ public class DropdownDataGeneric : BaseModel where TValue : Enum foreach (var value in enumValues) { var key = keyPrefix + value; - var display = InternationalizationManager.Instance.GetTranslation(key); + var display = App.API.GetTranslation(key); data.Add(new TR { Display = display, Value = value, LocalizationKey = key }); } @@ -30,7 +29,7 @@ public static void UpdateLabels(List options) where TR : DropdownDataGen { foreach (var item in options) { - item.Display = InternationalizationManager.Instance.GetTranslation(item.LocalizationKey); + item.Display = App.API.GetTranslation(item.LocalizationKey); } } } diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index de4f158ad04..cec8c318c59 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Windows.Forms; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core; @@ -30,6 +31,7 @@ public class SearchWindowScreenData : DropdownDataGeneric { public class SearchWindowAlignData : DropdownDataGeneric { } public class SearchPrecisionData : DropdownDataGeneric { } public class LastQueryModeData : DropdownDataGeneric { } + public class SearchDelayTimeData : DropdownDataGeneric { } public bool StartFlowLauncherOnSystemStartup { @@ -142,12 +144,33 @@ public bool PortableMode public List LastQueryModes { get; } = DropdownDataGeneric.GetValues("LastQuery"); + public List SearchDelayTimes { get; } = + DropdownDataGeneric.GetValues("SearchDelayTime"); + + public SearchDelayTimeData SearchDelayTime + { + get => SearchDelayTimes.FirstOrDefault(x => x.Value == Settings.SearchDelayTime) ?? + SearchDelayTimes.FirstOrDefault(x => x.Value == Plugin.SearchDelayTime.Normal) ?? + SearchDelayTimes.FirstOrDefault(); + set + { + if (value == null) + return; + + if (Settings.SearchDelayTime != value.Value) + { + Settings.SearchDelayTime = value.Value; + } + } + } + private void UpdateEnumDropdownLocalizations() { DropdownDataGeneric.UpdateLabels(SearchWindowScreens); DropdownDataGeneric.UpdateLabels(SearchWindowAligns); DropdownDataGeneric.UpdateLabels(SearchPrecisionScores); DropdownDataGeneric.UpdateLabels(LastQueryModes); + DropdownDataGeneric.UpdateLabels(SearchDelayTimes); } public string Language diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs index dd9e5786de1..3c1aba400a0 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs @@ -30,8 +30,9 @@ public SettingsPanePluginsViewModel(Settings settings) .Select(plugin => new PluginViewModel { PluginPair = plugin, - PluginSettingsObject = _settings.PluginSettings.Plugins[plugin.Metadata.ID] + PluginSettingsObject = _settings.PluginSettings.GetPluginSettings(plugin.Metadata.ID) }) + .Where(plugin => plugin.PluginSettingsObject != null) .ToList(); public List FilteredPluginViewModels => PluginViewModels diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index a80e618e8b4..2b198474ba8 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -172,6 +172,30 @@ OnContent="{DynamicResource enable}" /> + + + + + + + + + + + Results.PropertyChanged += (o, args) => { switch (args.PropertyName) { @@ -171,7 +171,7 @@ public MainViewModel() } }; - History.PropertyChanged += (_, args) => + History.PropertyChanged += (o, args) => { switch (args.PropertyName) { @@ -298,14 +298,16 @@ public void ReQuery() { if (QueryResultsSelected()) { - _ = QueryResultsAsync(isReQuery: true); + // When we are re-querying, we should not delay the query + _ = QueryResultsAsync(false, isReQuery: true); } } public void ReQuery(bool reselect) { BackToQueryResults(); - _ = QueryResultsAsync(isReQuery: true, reSelect: reselect); + // When we are re-querying, we should not delay the query + _ = QueryResultsAsync(false, isReQuery: true, reSelect: reselect); } [RelayCommand] @@ -313,7 +315,7 @@ public void ReverseHistory() { if (_history.Items.Count > 0) { - ChangeQueryText(_history.Items[^lastHistoryIndex].Query.ToString()); + ChangeQueryText(_history.Items[^lastHistoryIndex].Query); if (lastHistoryIndex < _history.Items.Count) { lastHistoryIndex++; @@ -326,7 +328,7 @@ public void ForwardHistory() { if (_history.Items.Count > 0) { - ChangeQueryText(_history.Items[^lastHistoryIndex].Query.ToString()); + ChangeQueryText(_history.Items[^lastHistoryIndex].Query); if (lastHistoryIndex > 1) { lastHistoryIndex--; @@ -577,7 +579,6 @@ public string QueryText { _queryText = value; OnPropertyChanged(); - Query(); } } @@ -646,19 +647,21 @@ private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false return; } - BackToQueryResults(); - if (QueryText != queryText) { - // re-query is done in QueryText's setter method + // Change query text first QueryText = queryText; + // When we are changing query from codes, we should not delay the query + await QueryAsync(false, isReQuery: false); + // set to false so the subsequent set true triggers // PropertyChanged and MoveQueryTextToEnd is called QueryTextCursorMovedToEnd = false; } else if (isReQuery) { - await QueryAsync(isReQuery: true); + // When we are re-querying, we should not delay the query + await QueryAsync(false, isReQuery: true); } QueryTextCursorMovedToEnd = true; @@ -727,14 +730,10 @@ private ResultsViewModel SelectedResults // setter won't be called when property value is not changed. // so we need manually call Query() // http://stackoverflow.com/posts/25895769/revisions - if (string.IsNullOrEmpty(QueryText)) - { - Query(); - } - else - { - QueryText = string.Empty; - } + QueryText = string.Empty; + // When we are changing query because selected results are changed to history or context menu, + // we should not delay the query + Query(false); if (HistorySelected()) { @@ -754,7 +753,7 @@ private ResultsViewModel SelectedResults public Visibility ProgressBarVisibility { get; set; } public Visibility MainWindowVisibility { get; set; } - // This is to be used for determining the visibility status of the mainwindow instead of MainWindowVisibility + // This is to be used for determining the visibility status of the main window instead of MainWindowVisibility // because it is more accurate and reliable representation than using Visibility as a condition check public bool MainWindowVisibilityStatus { get; set; } = true; @@ -1041,16 +1040,16 @@ private bool QueryResultsPreviewed() #region Query - private void Query(bool isReQuery = false) + public void Query(bool searchDelay, bool isReQuery = false) { - _ = QueryAsync(isReQuery); + _ = QueryAsync(searchDelay, isReQuery); } - private async Task QueryAsync(bool isReQuery = false) + private async Task QueryAsync(bool searchDelay, bool isReQuery = false) { if (QueryResultsSelected()) { - await QueryResultsAsync(isReQuery); + await QueryResultsAsync(searchDelay, isReQuery); } else if (ContextMenuSelected()) { @@ -1072,9 +1071,7 @@ private void QueryContextMenu() if (selected != null) // SelectedItem returns null if selection is empty. { - List results; - - results = PluginManager.GetContextMenusForPlugin(selected); + var results = PluginManager.GetContextMenusForPlugin(selected); results.Add(ContextMenuTopMost(selected)); results.Add(ContextMenuPluginInfo(selected.PluginID)); @@ -1151,13 +1148,18 @@ private void QueryHistory() } } - private async Task QueryResultsAsync(bool isReQuery = false, bool reSelect = true) + private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { + // TODO: Remove debug codes. + System.Diagnostics.Debug.WriteLine("!!!QueryResults"); + _updateSource?.Cancel(); var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); - if (query == null) // shortcut expanded + var plugins = PluginManager.ValidPluginsForQuery(query); + + if (query == null || plugins.Count == 0) // shortcut expanded { Results.Clear(); Results.Visibility = Visibility.Collapsed; @@ -1166,6 +1168,18 @@ private async Task QueryResultsAsync(bool isReQuery = false, bool reSelect = tru SearchIconVisibility = Visibility.Visible; return; } + else if (plugins.Count == 1) + { + PluginIconPath = plugins.Single().Metadata.IcoPath; + PluginIconSource = await ImageLoader.LoadAsync(PluginIconPath); + SearchIconVisibility = Visibility.Hidden; + } + else + { + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; + } _updateSource?.Dispose(); @@ -1190,21 +1204,6 @@ private async Task QueryResultsAsync(bool isReQuery = false, bool reSelect = tru _lastQuery = query; - var plugins = PluginManager.ValidPluginsForQuery(query); - - if (plugins.Count == 1) - { - PluginIconPath = plugins.Single().Metadata.IcoPath; - PluginIconSource = await ImageLoader.LoadAsync(PluginIconPath); - SearchIconVisibility = Visibility.Hidden; - } - else - { - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; - } - if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign) { // Wait 45 millisecond for query change in global query @@ -1215,22 +1214,36 @@ private async Task QueryResultsAsync(bool isReQuery = false, bool reSelect = tru } _ = 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) { - ProgressBarVisibility = Visibility.Visible; - } - }, _updateSource.Token, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); + // 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) + { + ProgressBarVisibility = Visibility.Visible; + } + }, + _updateSource.Token, + TaskContinuationOptions.NotOnCanceled, + TaskScheduler.Default); - // plugins is ICollection, meaning LINQ will get the Count and preallocate Array + // plugins are ICollection, meaning LINQ will get the Count and preallocate Array var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch { - false => QueryTaskAsync(plugin, reSelect), + false => QueryTaskAsync(plugin, _updateSource.Token), true => Task.CompletedTask }).ToArray(); + // TODO: Remove debug codes. + System.Diagnostics.Debug.Write($"!!!Querying {query.RawQuery}: search delay {searchDelay}"); + foreach (var plugin in plugins) + { + if (!plugin.Metadata.Disabled) + { + System.Diagnostics.Debug.Write($"{plugin.Metadata.Name}, "); + } + } + System.Diagnostics.Debug.Write("\n"); + try { // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first @@ -1254,16 +1267,43 @@ private async Task QueryResultsAsync(bool isReQuery = false, bool reSelect = tru } // Local function - async Task QueryTaskAsync(PluginPair plugin, bool reSelect = true) + async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) { + if (searchDelay) + { + var searchDelayTime = (plugin.Metadata.SearchDelayTime ?? Settings.SearchDelayTime) switch + { + SearchDelayTime.VeryLong => 250, + SearchDelayTime.Long => 200, + SearchDelayTime.Normal => 150, + SearchDelayTime.Short => 100, + SearchDelayTime.VeryShort => 50, + _ => 150 + }; + + // TODO: Remove debug codes. + System.Diagnostics.Debug.WriteLine($"!!!{plugin.Metadata.Name} Waiting {searchDelayTime} ms"); + + await Task.Delay(searchDelayTime, token); + + // TODO: Remove debug codes. + System.Diagnostics.Debug.WriteLine($"!!!{plugin.Metadata.Name} Waited {searchDelayTime} ms"); + + if (token.IsCancellationRequested) + return; + } + // 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(); + // TODO: Remove debug codes. + System.Diagnostics.Debug.WriteLine($"!!!{query.RawQuery} Querying {plugin.Metadata.Name}"); IReadOnlyList results = - await PluginManager.QueryForPluginAsync(plugin, query, _updateSource.Token); + await PluginManager.QueryForPluginAsync(plugin, query, token); - _updateSource.Token.ThrowIfCancellationRequested(); + if (token.IsCancellationRequested) + return; IReadOnlyList resultsCopy; if (results == null) @@ -1273,11 +1313,11 @@ async Task QueryTaskAsync(PluginPair plugin, bool reSelect = true) else { // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. - resultsCopy = DeepCloneResults(results); + resultsCopy = DeepCloneResults(results, token); } if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, - _updateSource.Token, reSelect))) + token, reSelect))) { Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); } @@ -1591,7 +1631,7 @@ public void Save() } /// - /// To avoid deadlock, this method should not called from main thread + /// To avoid deadlock, this method should not be called from main thread /// public void UpdateResultView(ICollection resultsForUpdates) { diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 33d7306040c..e91badb3894 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -1,12 +1,12 @@ using System.Linq; +using System.Threading.Tasks; using System.Windows; -using System.Windows.Media; -using Flow.Launcher.Plugin; -using Flow.Launcher.Infrastructure.Image; -using Flow.Launcher.Core.Plugin; using System.Windows.Controls; +using System.Windows.Media; using CommunityToolkit.Mvvm.Input; -using Flow.Launcher.Core.Resource; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.Image; +using Flow.Launcher.Plugin; using Flow.Launcher.Resources.Controls; namespace Flow.Launcher.ViewModel @@ -28,7 +28,7 @@ public PluginPair PluginPair } } - private string PluginManagerActionKeyword + private static string PluginManagerActionKeyword { get { @@ -43,9 +43,10 @@ private string PluginManagerActionKeyword } } - private async void LoadIconAsync() + private async Task LoadIconAsync() { Image = await ImageLoader.LoadAsync(PluginPair.Metadata.IcoPath); + OnPropertyChanged(nameof(Image)); } public ImageSource Image @@ -53,12 +54,13 @@ public ImageSource Image get { if (_image == ImageLoader.MissingImage) - LoadIconAsync(); + _ = LoadIconAsync(); return _image; } set => _image = value; } + public bool PluginState { get => !PluginPair.Metadata.Disabled; @@ -68,6 +70,7 @@ public bool PluginState PluginSettingsObject.Disabled = !value; } } + public bool IsExpanded { get => _isExpanded; @@ -80,6 +83,16 @@ public bool IsExpanded } } + public SearchDelayTime? PluginSearchDelayTime + { + get => PluginPair.Metadata.SearchDelayTime; + set + { + PluginPair.Metadata.SearchDelayTime = value; + PluginSettingsObject.SearchDelayTime = value; + } + } + private Control _settingControl; private bool _isExpanded; @@ -87,9 +100,13 @@ public bool IsExpanded public Control BottomPart1 => IsExpanded ? _bottomPart1 ??= new InstalledPluginDisplayKeyword() : null; private Control _bottomPart2; - public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null; + public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginSearchDelay() : null; + + private Control _bottomPart3; + public Control BottomPart3 => IsExpanded ? _bottomPart3 ??= new InstalledPluginDisplayBottomData() : null; - public bool HasSettingControl => PluginPair.Plugin is ISettingProvider && (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel()); + public bool HasSettingControl => PluginPair.Plugin is ISettingProvider && + (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel()); public Control SettingControl => IsExpanded ? _settingControl @@ -99,20 +116,33 @@ public Control SettingControl : null; private ImageSource _image = ImageLoader.MissingImage; - public Visibility ActionKeywordsVisibility => PluginPair.Metadata.HideActionKeywordPanel ? Visibility.Collapsed : Visibility.Visible; - public string InitilizaTime => PluginPair.Metadata.InitTime + "ms"; + public Visibility ActionKeywordsVisibility => PluginPair.Metadata.HideActionKeywordPanel ? + Visibility.Collapsed : Visibility.Visible; + public string InitializeTime => PluginPair.Metadata.InitTime + "ms"; public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms"; - public string Version => InternationalizationManager.Instance.GetTranslation("plugin_query_version") + " " + PluginPair.Metadata.Version; - public string InitAndQueryTime => InternationalizationManager.Instance.GetTranslation("plugin_init_time") + " " + PluginPair.Metadata.InitTime + "ms, " + InternationalizationManager.Instance.GetTranslation("plugin_query_time") + " " + PluginPair.Metadata.AvgQueryTime + "ms"; + public string Version => App.API.GetTranslation("plugin_query_version") + " " + PluginPair.Metadata.Version; + public string InitAndQueryTime => + App.API.GetTranslation("plugin_init_time") + " " + + PluginPair.Metadata.InitTime + "ms, " + + App.API.GetTranslation("plugin_query_time") + " " + + PluginPair.Metadata.AvgQueryTime + "ms"; public string ActionKeywordsText => string.Join(Query.ActionKeywordSeparator, PluginPair.Metadata.ActionKeywords); public int Priority => PluginPair.Metadata.Priority; - public Infrastructure.UserSettings.Plugin PluginSettingsObject { get; set; } + public string SearchDelayTimeText => PluginPair.Metadata.SearchDelayTime == null ? + App.API.GetTranslation("default") : + App.API.GetTranslation($"SearchDelayTime{PluginPair.Metadata.SearchDelayTime}"); + public Infrastructure.UserSettings.Plugin PluginSettingsObject{ get; init; } public void OnActionKeywordsChanged() { OnPropertyChanged(nameof(ActionKeywordsText)); } + public void OnSearchDelayTimeChanged() + { + OnPropertyChanged(nameof(SearchDelayTimeText)); + } + public void ChangePriority(int newPriority) { PluginPair.Metadata.Priority = newPriority; @@ -123,7 +153,7 @@ public void ChangePriority(int newPriority) [RelayCommand] private void EditPluginPriority() { - PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(PluginPair. Metadata.ID, this); + var priorityChangeWindow = new PriorityChangeWindow(PluginPair. Metadata.ID, this); priorityChangeWindow.ShowDialog(); } @@ -151,8 +181,15 @@ private void OpenDeletePluginWindow() [RelayCommand] private void SetActionKeywords() { - ActionKeywords changeKeywordsWindow = new ActionKeywords(this); + var changeKeywordsWindow = new ActionKeywords(this); changeKeywordsWindow.ShowDialog(); } + + [RelayCommand] + private void SetSearchDelayTime() + { + var searchDelayTimeWindow = new SearchDelayTimeWindow(this); + searchDelayTimeWindow.ShowDialog(); + } } } diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json index 20f6def705e..64681f803b8 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json @@ -31,5 +31,6 @@ "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll", - "IcoPath": "Images\\web_search.png" + "IcoPath": "Images\\web_search.png", + "SearchDelayTime": "VeryLong" }