diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index ae7b098a206..470a137de14 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1137,7 +1137,9 @@ private void SetupResizeMode() private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) { var textBox = (TextBox)sender; - _viewModel.QueryText = textBox.Text; + var text = textBox.Text; + _viewModel.QueryText = text; + _viewModel.ResetSelectionIfQueryEmpty(text); _viewModel.Query(_settings.SearchQueryResultsWithDelay); } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 2f1ed0f5103..fa32268a1ee 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -34,6 +34,7 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable private bool _isQueryRunning; private Query _lastQuery; private string _queryTextBeforeLeaveResults; + private bool _blockQueryExecution = false; private readonly FlowLauncherJsonStorage _historyItemsStorage; private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; @@ -640,7 +641,7 @@ private void DecreaseMaxResult() /// /// /// Force query even when Query Text doesn't change - public void ChangeQueryText(string queryText, bool isReQuery = false) + public void ChangeQueryText(string queryText, bool isReQuery = false, bool suppressQueryExecution = false) { // Must check access so that we will not block the UI thread which causes window visibility issue if (!Application.Current.Dispatcher.CheckAccess()) @@ -668,16 +669,15 @@ public void ChangeQueryText(string queryText, bool isReQuery = false) QueryTextCursorMovedToEnd = true; } - /// /// Async version of /// - private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false) + private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false, bool suppressQueryExecution = false) { // 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(() => ChangeQueryTextAsync(queryText, isReQuery)); + await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryTextAsync(queryText, isReQuery, suppressQueryExecution)); return; } @@ -685,14 +685,14 @@ private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false { // 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 + ResetSelectionIfQueryEmpty(queryText); + if (!suppressQueryExecution) + { + await QueryAsync(false, isReQuery: false); + } QueryTextCursorMovedToEnd = false; } - else if (isReQuery) + else if (isReQuery && !suppressQueryExecution) { // When we are re-querying, we should not delay the query await QueryAsync(false, isReQuery: true); @@ -707,6 +707,8 @@ private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false public bool QueryTextCursorMovedToEnd { get; set; } private ResultsViewModel _selectedResults; + private ResultViewModel _lastSelectedResultItem; + private int _lastSelectedResultIndex = -1; private ResultsViewModel SelectedResults { @@ -716,6 +718,14 @@ private ResultsViewModel SelectedResults var isReturningFromQueryResults = QueryResultsSelected(); var isReturningFromContextMenu = ContextMenuSelected(); var isReturningFromHistory = HistorySelected(); + + // save the index of the selected index before changing the selected results + if (QueryResultsSelected() && value != Results) + { + _lastSelectedResultItem = Results.SelectedItem; + _lastSelectedResultIndex = Results.SelectedIndex; + } + _selectedResults = value; if (QueryResultsSelected()) { @@ -727,11 +737,25 @@ private ResultsViewModel SelectedResults // result from the one that was selected before going into the context menu to the first result. // The code below correctly restores QueryText and puts the text caret at the end without // running the query again when returning from the context menu. - if (isReturningFromContextMenu) + if (isReturningFromContextMenu && _lastSelectedResultItem != null) { - _queryText = _queryTextBeforeLeaveResults; - OnPropertyChanged(nameof(QueryText)); - QueryTextCursorMovedToEnd = true; + try + { + _blockQueryExecution = true; + // Set querytext without running the query for keep index. if not use this, index will be 0. + _queryText = _queryTextBeforeLeaveResults; + OnPropertyChanged(nameof(QueryText)); + QueryTextCursorMovedToEnd = true; + } + finally + { + _blockQueryExecution = false; + } + // restore selected item + if (_lastSelectedResultIndex >= 0 && _lastSelectedResultIndex < Results.Results.Count) + { + Results.SelectedIndex = _lastSelectedResultIndex; + } } else { @@ -1092,6 +1116,8 @@ public void Query(bool searchDelay, bool isReQuery = false) private async Task QueryAsync(bool searchDelay, bool isReQuery = false) { + if (_blockQueryExecution) + return; if (QueryResultsSelected()) { await QueryResultsAsync(searchDelay, isReQuery); @@ -1348,6 +1374,14 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) } } } + + public void ResetSelectionIfQueryEmpty(string queryText) + { + if (string.IsNullOrEmpty(queryText)) + { + Results.ResetSelectedIndex(); + } + } private async Task ConstructQueryAsync(string queryText, IEnumerable customShortcuts, IEnumerable builtInShortcuts) @@ -1683,67 +1717,27 @@ public void Save() /// public void UpdateResultView(ICollection resultsForUpdates) { - if (!resultsForUpdates.Any()) + if (resultsForUpdates == null || !resultsForUpdates.Any()) return; - CancellationToken token; + // Block the query execution when Open ContextMenu is called + if (!QueryResultsSelected()) + return; + + CancellationToken token; try { - // Don't know why sometimes even resultsForUpdates is empty, the method won't return; token = resultsForUpdates.Select(r => r.Token).Distinct().SingleOrDefault(); } -#if DEBUG - catch - { - throw new ArgumentException("Unacceptable token"); - } -#else catch { token = default; } -#endif - - foreach (var metaResults in resultsForUpdates) - { - foreach (var result in metaResults.Results) - { - if (_topMostRecord.IsTopMost(result)) - { - result.Score = Result.MaxScore; - } - else - { - var priorityScore = metaResults.Metadata.Priority * 150; - if (result.AddSelectedCount) - { - if ((long)result.Score + _userSelectedRecord.GetSelectedCount(result) + priorityScore > Result.MaxScore) - { - result.Score = Result.MaxScore; - } - else - { - result.Score += _userSelectedRecord.GetSelectedCount(result) + priorityScore; - } - } - else - { - if ((long)result.Score + priorityScore > Result.MaxScore) - { - result.Score = Result.MaxScore; - } - else - { - result.Score += priorityScore; - } - } - } - } - } - // it should be the same for all results + // Reselect the first result if the first result is selected bool reSelect = resultsForUpdates.First().ReSelectFirstResult; + // Update results while remembering the currently selected item Results.AddResults(resultsForUpdates, token, reSelect); } diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 02fb379fa07..e8b00aeba64 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -22,6 +22,9 @@ public class ResultsViewModel : BaseModel private readonly object _collectionLock = new(); private readonly Settings _settings; private int MaxResults => _settings?.MaxResultsToShow ?? 6; + + private ResultViewModel _lastSelectedItem; + private int _lastSelectedIndex = -1; public ResultsViewModel() { @@ -170,6 +173,19 @@ public void KeepResultsExcept(PluginMetadata metadata) Results.Update(Results.Where(r => r.Result.PluginID != metadata.ID).ToList()); } + public void ResetSelectedIndex() + { + _lastSelectedIndex = 0; + _lastSelectedItem = null; // prevent accidental reselection of stale item + if (Results.Any()) + { + SelectedIndex = 0; + SelectedItem = Results[0]; + } + OnPropertyChanged(nameof(SelectedIndex)); + OnPropertyChanged(nameof(SelectedItem)); + } + /// /// To avoid deadlock, this method should not called from main thread /// @@ -184,36 +200,97 @@ public void AddResults(List newRawResults, string resultId) /// public void AddResults(ICollection resultsForUpdates, CancellationToken token, bool reselect = true) { + if (resultsForUpdates == null || !resultsForUpdates.Any()) + return; + + // Save the currently selected item + ResultViewModel lastSelectedItem = null; + int lastSelectedIndex = -1; + + Application.Current.Dispatcher.Invoke(() => + { + if (SelectedItem != null) + { + lastSelectedItem = SelectedItem; + lastSelectedIndex = SelectedIndex; + } + }); + + _lastSelectedItem = lastSelectedItem; + _lastSelectedIndex = lastSelectedIndex; + + // Generate new results var newResults = NewResults(resultsForUpdates); if (token.IsCancellationRequested) return; + // Update results (includes logic for restoring selection) UpdateResults(newResults, reselect, token); } - private void UpdateResults(List newResults, bool reselect = true, CancellationToken token = default) + private void UpdateResults(List newResults, bool reselect = true, + CancellationToken token = default) { lock (_collectionLock) { - // update UI in one run, so it can avoid UI flickering + // Update previous results and UI Results.Update(newResults, token); + // Only perform selection logic if reselect is true if (reselect && Results.Any()) - SelectedItem = Results[0]; + { + // If a previously selected item exists and still remains in the list, reselect it + if (_lastSelectedItem != null && Results.Contains(_lastSelectedItem)) + { + SelectedItem = _lastSelectedItem; + SelectedIndex = Results.IndexOf(_lastSelectedItem); + } + // If previous index is still within valid range, use that index + else if (_lastSelectedIndex >= 0 && _lastSelectedIndex < Results.Count) + { + SelectedIndex = _lastSelectedIndex; + SelectedItem = Results[SelectedIndex]; + } + // If nothing else is valid, select the first item + else if (Results.Count > 0) + { + SelectedItem = Results[0]; + SelectedIndex = 0; + } + } } - - switch (Visibility) + + // If no item is selected, select the first one + if (Results.Count > 0 && (SelectedIndex == -1 || SelectedItem == null)) { - case Visibility.Collapsed when Results.Count > 0: + SelectedIndex = 0; + SelectedItem = Results[0]; + } + // Visibility update - fix for related issue + if (Results.Count > 0) + { + // 1. Always ensure index is valid when there are results + if (SelectedIndex == -1 || SelectedItem == null) + { SelectedIndex = 0; + SelectedItem = Results[0]; + } + // 2. Update visibility + if (Visibility == Visibility.Collapsed) + { Visibility = Visibility.Visible; - break; - case Visibility.Visible when Results.Count == 0: - Visibility = Visibility.Collapsed; - break; + } + } + else if (Visibility == Visibility.Visible && Results.Count == 0) + { + Visibility = Visibility.Collapsed; } + // Notify property changes to update UI bindings + OnPropertyChanged(nameof(SelectedIndex)); + OnPropertyChanged(nameof(SelectedItem)); } + private List NewResults(List newRawResults, string resultId) { if (newRawResults.Count == 0)