diff --git a/src/modules/launcher/PowerLauncher/MainWindow.xaml.cs b/src/modules/launcher/PowerLauncher/MainWindow.xaml.cs index 58a21993fd..77305eb7d8 100644 --- a/src/modules/launcher/PowerLauncher/MainWindow.xaml.cs +++ b/src/modules/launcher/PowerLauncher/MainWindow.xaml.cs @@ -159,15 +159,16 @@ namespace PowerLauncher SearchBox.QueryTextBox.DataContext = _viewModel; SearchBox.QueryTextBox.PreviewKeyDown += Launcher_KeyDown; - SetupSearchTextBoxReactiveness(_viewModel.GetSearchQueryResultsWithDelaySetting(), _viewModel.GetSearchInputDelaySetting()); + + SetupSearchTextBoxReactiveness(_viewModel.GetSearchQueryResultsWithDelaySetting()); _viewModel.RegisterSettingsChangeListener( (s, prop_e) => { - if (prop_e.PropertyName == nameof(PowerToysRunSettings.SearchQueryResultsWithDelay) || prop_e.PropertyName == nameof(PowerToysRunSettings.SearchInputDelay)) + if (prop_e.PropertyName == nameof(PowerToysRunSettings.SearchQueryResultsWithDelay) || prop_e.PropertyName == nameof(PowerToysRunSettings.SearchInputDelay) || prop_e.PropertyName == nameof(PowerToysRunSettings.SearchInputDelayFast)) { Application.Current.Dispatcher.Invoke(() => { - SetupSearchTextBoxReactiveness(_viewModel.GetSearchQueryResultsWithDelaySetting(), _viewModel.GetSearchInputDelaySetting()); + SetupSearchTextBoxReactiveness(_viewModel.GetSearchQueryResultsWithDelaySetting()); }); } }); @@ -191,7 +192,7 @@ namespace PowerLauncher BringProcessToForeground(); } - private void SetupSearchTextBoxReactiveness(bool showResultsWithDelay, int searchInputDelayMs) + private void SetupSearchTextBoxReactiveness(bool showResultsWithDelay) { if (_reactiveSubscription != null) { @@ -206,10 +207,54 @@ namespace PowerLauncher _reactiveSubscription = Observable.FromEventPattern( add => SearchBox.QueryTextBox.TextChanged += add, remove => SearchBox.QueryTextBox.TextChanged -= remove) + .Do(@event => ClearAutoCompleteText((TextBox)@event.Sender)) + .Throttle(TimeSpan.FromMilliseconds(_settings.SearchInputDelayFast)) + .Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, false))) + .Throttle(TimeSpan.FromMilliseconds(_settings.SearchInputDelay)) + .Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, true))) + .Subscribe(); + + /* + if (_settings.PTRSearchQueryFastResultsWithDelay) + { + // old mode, delay fast and delayed execution + _reactiveSubscription = Observable.FromEventPattern( + add => SearchBox.QueryTextBox.TextChanged += add, + remove => SearchBox.QueryTextBox.TextChanged -= remove) .Do(@event => ClearAutoCompleteText((TextBox)@event.Sender)) .Throttle(TimeSpan.FromMilliseconds(searchInputDelayMs)) .Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender))) .Subscribe(); + } + else + { + if (_settings.PTRSearchQueryFastResultsWithPartialDelay) + { + // new mode, fire non-delayed right away, and then later the delayed execution + _reactiveSubscription = Observable.FromEventPattern( + add => SearchBox.QueryTextBox.TextChanged += add, + remove => SearchBox.QueryTextBox.TextChanged -= remove) + .Do(@event => ClearAutoCompleteText((TextBox)@event.Sender)) + .Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, false))) + .Throttle(TimeSpan.FromMilliseconds(searchInputDelayMs)) + .Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, true))) + .Subscribe(); + } + else + { + // new mode, fire non-delayed after short delay, and then later the delayed execution + _reactiveSubscription = Observable.FromEventPattern( + add => SearchBox.QueryTextBox.TextChanged += add, + remove => SearchBox.QueryTextBox.TextChanged -= remove) + .Do(@event => ClearAutoCompleteText((TextBox)@event.Sender)) + .Throttle(TimeSpan.FromMilliseconds(_settings.SearchInputDelayFast)) + .Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, false))) + .Throttle(TimeSpan.FromMilliseconds(searchInputDelayMs)) + .Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, true))) + .Subscribe(); + } + } + */ } else { @@ -475,21 +520,79 @@ namespace PowerLauncher { SearchBox.AutoCompleteTextBlock.Text = string.Empty; } + + var showResultsWithDelay = _viewModel.GetSearchQueryResultsWithDelaySetting(); + + // only if we are using throttled search and throttled 'fast' search, do we need to do anything different with the current results. + if (showResultsWithDelay && _settings.PTRSearchQueryFastResultsWithDelay) + { + // Default means we don't do anything we did not do before... leave the results as is, they will be changed as needed when results are returned + var pTRunStartNewSearchAction = _settings.PTRunStartNewSearchAction ?? "Default"; + + if (pTRunStartNewSearchAction == "DeSelect") + { + // leave the results, be deselect anything to it will not be activated by key, can still be arrow-key or clicked though + if (!_isTextSetProgrammatically) + { + DeselectAllResults(); + } + } + else if (pTRunStartNewSearchAction == "Clear") + { + // remove all results to prepare for new results, this causes flashing usually and is not cool + if (!_isTextSetProgrammatically) + { + ClearResults(); + } + } + } + } + + private void ClearResults() + { + _viewModel.Results.SelectedItem = null; + System.Threading.Tasks.Task.Run(() => + { + Application.Current.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() => + { + _viewModel.Results.Clear(); + _viewModel.Results.Results.NotifyChanges(); + })); + }); + } + + private void DeselectAllResults() + { + Application.Current.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() => + { + _viewModel.Results.SelectedIndex = -1; + })); } private void PerformSearchQuery(TextBox textBox) + { + PerformSearchQuery(textBox, null); + } + + private void PerformSearchQuery(TextBox textBox, bool? delayedExecution) { var text = textBox.Text; if (_isTextSetProgrammatically) { textBox.SelectionStart = textBox.Text.Length; - _isTextSetProgrammatically = false; + + // because IF this is delayedExecution = false (run fast queries) we know this will be called again with delayedExecution = true + // if we don't do this, the second (partner) call will not be called _isTextSetProgrammatically = true also, and we need it to. + if (delayedExecution.HasValue && delayedExecution.Value) + { + _isTextSetProgrammatically = false; + } } else { _viewModel.QueryText = text; - _viewModel.Query(); + _viewModel.Query(delayedExecution); } } diff --git a/src/modules/launcher/PowerLauncher/SettingsReader.cs b/src/modules/launcher/PowerLauncher/SettingsReader.cs index 433ad8a832..491315fba0 100644 --- a/src/modules/launcher/PowerLauncher/SettingsReader.cs +++ b/src/modules/launcher/PowerLauncher/SettingsReader.cs @@ -115,6 +115,11 @@ namespace PowerLauncher _settings.SearchInputDelay = overloadSettings.Properties.SearchInputDelay; } + if (_settings.SearchInputDelayFast != overloadSettings.Properties.SearchInputDelayFast) + { + _settings.SearchInputDelayFast = overloadSettings.Properties.SearchInputDelayFast; + } + if (_settings.SearchClickedItemWeight != overloadSettings.Properties.SearchClickedItemWeight) { _settings.SearchClickedItemWeight = overloadSettings.Properties.SearchClickedItemWeight; diff --git a/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs b/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs index 5daef63896..4ee4d2ef3b 100644 --- a/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs +++ b/src/modules/launcher/PowerLauncher/ViewModel/MainViewModel.cs @@ -455,10 +455,15 @@ namespace PowerLauncher.ViewModel } public void Query() + { + Query(null); + } + + public void Query(bool? delayedExecution) { if (SelectedIsFromQueryResults()) { - QueryResults(); + QueryResults(delayedExecution); } else if (HistorySelected()) { @@ -510,6 +515,11 @@ namespace PowerLauncher.ViewModel } private void QueryResults() + { + QueryResults(null); + } + + private void QueryResults(bool? delayedExecution) { var queryTuning = GetQueryTuningOptions(); var doFinalSort = queryTuning.SearchQueryTuningEnabled && queryTuning.SearchWaitForSlowResults; @@ -542,20 +552,44 @@ namespace PowerLauncher.ViewModel // Contains all the plugins for which this raw query is valid var plugins = pluginQueryPairs.Keys.ToList(); + var sw = System.Diagnostics.Stopwatch.StartNew(); + try { - currentCancellationToken.ThrowIfCancellationRequested(); + var resultPluginPair = new System.Collections.Concurrent.ConcurrentDictionary>(); - var resultPluginPair = new List<(List, PluginMetadata)>(); - - // To execute a query corresponding to each plugin - foreach (KeyValuePair pluginQueryItem in pluginQueryPairs) + if (_settings.PTRunNonDelayedSearchInParallel) + { + Parallel.ForEach(pluginQueryPairs, (pluginQueryItem) => + { + try + { + var plugin = pluginQueryItem.Key; + var query = pluginQueryItem.Value; + var results = PluginManager.QueryForPlugin(plugin, query); + resultPluginPair[plugin.Metadata] = results; + currentCancellationToken.ThrowIfCancellationRequested(); + } + catch (OperationCanceledException) + { + // nothing to do here + } + }); + sw.Stop(); + } + else { - var plugin = pluginQueryItem.Key; - var query = pluginQueryItem.Value; - var results = PluginManager.QueryForPlugin(plugin, query); - resultPluginPair.Add((results, plugin.Metadata)); currentCancellationToken.ThrowIfCancellationRequested(); + + // To execute a query corresponding to each plugin + foreach (KeyValuePair pluginQueryItem in pluginQueryPairs) + { + var plugin = pluginQueryItem.Key; + var query = pluginQueryItem.Value; + var results = PluginManager.QueryForPlugin(plugin, query); + resultPluginPair[plugin.Metadata] = results; + currentCancellationToken.ThrowIfCancellationRequested(); + } } lock (_addResultsLock) @@ -566,7 +600,7 @@ namespace PowerLauncher.ViewModel Results.Clear(); foreach (var p in resultPluginPair) { - UpdateResultView(p.Item1, queryText, currentCancellationToken); + UpdateResultView(p.Value, queryText, currentCancellationToken); currentCancellationToken.ThrowIfCancellationRequested(); } @@ -588,54 +622,56 @@ namespace PowerLauncher.ViewModel bool noInitialResults = numResults == 0; - // Run the slower query of the DelayedExecution plugins - currentCancellationToken.ThrowIfCancellationRequested(); - Parallel.ForEach(plugins, (plugin) => - { - try + if (!delayedExecution.HasValue || delayedExecution.Value) + { + // Run the slower query of the DelayedExecution plugins + currentCancellationToken.ThrowIfCancellationRequested(); + Parallel.ForEach(plugins, (plugin) => { - Query query; - pluginQueryPairs.TryGetValue(plugin, out query); - - var results = PluginManager.QueryForPlugin(plugin, query, true); - currentCancellationToken.ThrowIfCancellationRequested(); - if ((results?.Count ?? 0) != 0) + try { - lock (_addResultsLock) + Query query; + pluginQueryPairs.TryGetValue(plugin, out query); + var results = PluginManager.QueryForPlugin(plugin, query, true); + currentCancellationToken.ThrowIfCancellationRequested(); + if ((results?.Count ?? 0) != 0) { - // Using CurrentCultureIgnoreCase since this is user facing - if (queryText.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) + lock (_addResultsLock) { - currentCancellationToken.ThrowIfCancellationRequested(); + // Using CurrentCultureIgnoreCase since this is user facing + if (queryText.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) + { + currentCancellationToken.ThrowIfCancellationRequested(); - // Remove the original results from the plugin - Results.Results.RemoveAll(r => r.Result.PluginID == plugin.Metadata.ID); - currentCancellationToken.ThrowIfCancellationRequested(); + // Remove the original results from the plugin + Results.Results.RemoveAll(r => r.Result.PluginID == plugin.Metadata.ID); + currentCancellationToken.ThrowIfCancellationRequested(); - // Add the new results from the plugin - UpdateResultView(results, queryText, currentCancellationToken); + // Add the new results from the plugin + UpdateResultView(results, queryText, currentCancellationToken); + + currentCancellationToken.ThrowIfCancellationRequested(); + numResults = Results.Results.Count; + if (!doFinalSort) + { + Results.Sort(queryTuning); + } + } currentCancellationToken.ThrowIfCancellationRequested(); - numResults = Results.Results.Count; if (!doFinalSort) { - Results.Sort(queryTuning); + UpdateResultsListViewAfterQuery(queryText, noInitialResults, true); } } - - currentCancellationToken.ThrowIfCancellationRequested(); - if (!doFinalSort) - { - UpdateResultsListViewAfterQuery(queryText, noInitialResults, true); - } } } - } - catch (OperationCanceledException) - { - // nothing to do here - } - }); + catch (OperationCanceledException) + { + // nothing to do here + } + }); + } } catch (OperationCanceledException) { @@ -1135,6 +1171,11 @@ namespace PowerLauncher.ViewModel return _settings.SearchQueryResultsWithDelay; } + public int GetSearchInputDelayFastSetting() + { + return _settings.SearchInputDelayFast; + } + public int GetSearchInputDelaySetting() { return _settings.SearchInputDelay; diff --git a/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs b/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs index 6c1bf2e856..5f4a5ab19a 100644 --- a/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs +++ b/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs @@ -82,12 +82,31 @@ namespace Wox.Infrastructure.UserSettings private int _searchInputDelay = 150; + private int _searchInputDelayFast = 30; + private int _searchClickedItemWeight = 5; private bool _searchQueryTuningEnabled; private bool _searchWaitForSlowResults; + public int SearchInputDelayFast + { + get + { + return _searchInputDelayFast; + } + + set + { + if (_searchInputDelayFast != value) + { + _searchInputDelayFast = value; + OnPropertyChanged(nameof(SearchInputDelayFast)); + } + } + } + public int SearchInputDelay { get @@ -168,6 +187,13 @@ namespace Wox.Infrastructure.UserSettings public string QueryBoxFontWeight { get; set; } + public bool PTRunNonDelayedSearchInParallel { get; set; } = true; + + public string PTRunStartNewSearchAction { get; set; } + + public bool PTRSearchQueryFastResultsWithDelay { get; set; } + + // public bool PTRSearchQueryFastResultsWithPartialDelay { get; set; } public string QueryBoxFontStretch { get; set; } public string ResultFont { get; set; } = FontFamily.GenericSansSerif.Name; diff --git a/src/settings-ui/Settings.UI.Library/PowerLauncherProperties.cs b/src/settings-ui/Settings.UI.Library/PowerLauncherProperties.cs index c915aa3b1c..d7c11b55c7 100644 --- a/src/settings-ui/Settings.UI.Library/PowerLauncherProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PowerLauncherProperties.cs @@ -57,6 +57,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("search_input_delay")] public int SearchInputDelay { get; set; } + [JsonPropertyName("search_input_delay_fast")] + public int SearchInputDelayFast { get; set; } + [JsonPropertyName("search_clicked_item_weight")] public int SearchClickedItemWeight { get; set; } diff --git a/src/settings-ui/Settings.UI.Library/ViewModels/PowerLauncherViewModel.cs b/src/settings-ui/Settings.UI.Library/ViewModels/PowerLauncherViewModel.cs index 4f7c222ba6..ce65d19022 100644 --- a/src/settings-ui/Settings.UI.Library/ViewModels/PowerLauncherViewModel.cs +++ b/src/settings-ui/Settings.UI.Library/ViewModels/PowerLauncherViewModel.cs @@ -313,6 +313,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels } } + public int SearchInputDelayFast + { + get + { + return settings.Properties.SearchInputDelayFast; + } + + set + { + if (settings.Properties.SearchInputDelayFast != value) + { + settings.Properties.SearchInputDelayFast = value; + UpdateSettings(); + } + } + } + public int SearchInputDelay { get diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 81a4354e04..3f0612ad61 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -415,16 +415,28 @@ Clear the previous query on launch - Delay search + Input Smoothing This is about adding a delay to wait for more input before executing a search - Add a delay to wait for more input before executing a search + Wait for more input before searching. This reduces interface jumpiness and system load. - - Search delay (ms) + + Immediate plugins + + + Affects the plugins that make the UI wait for their results by this amount. Recommended: 30-50 ms. + + + Background execution plugins + + + Affects the plugins that execute in the background by this amount. Recommended: 100-150 ms. + + + Fast plugin throttle (ms) ms = milliseconds - + To: Keyboard Manager mapping keys view right header diff --git a/src/settings-ui/Settings.UI/Views/PowerLauncherPage.xaml b/src/settings-ui/Settings.UI/Views/PowerLauncherPage.xaml index 9781179b06..ab49772f13 100644 --- a/src/settings-ui/Settings.UI/Views/PowerLauncherPage.xaml +++ b/src/settings-ui/Settings.UI/Views/PowerLauncherPage.xaml @@ -103,9 +103,23 @@ - - - + + + + + + + + + - - + + + + +