From b28814fe38c6807887af439edab043596f6acb0f Mon Sep 17 00:00:00 2001 From: DB p Date: Mon, 21 Apr 2025 18:38:24 +0900 Subject: [PATCH 1/7] Add Restoring SelectedItemIndex when loaded and contextmenu --- Flow.Launcher/ViewModel/MainViewModel.cs | 122 ++++++++++---------- Flow.Launcher/ViewModel/ResultsViewModel.cs | 83 +++++++++++-- 2 files changed, 136 insertions(+), 69 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 00675149b41..d1aea3bf995 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; @@ -638,20 +639,19 @@ 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) { - _ = ChangeQueryTextAsync(queryText, isReQuery); + _ = ChangeQueryTextAsync(queryText, isReQuery, suppressQueryExecution); } - /// /// 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 cause window visibility issue if (!Application.Current.Dispatcher.CheckAccess()) { - await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryText(queryText, isReQuery)); + await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryText(queryText, isReQuery, suppressQueryExecution)); return; } @@ -659,14 +659,24 @@ 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); + + if (string.IsNullOrEmpty(queryText)) + { + Results.ResetSelectedIndex(); + } + + // Check if we are in the process of changing query text + if (!suppressQueryExecution) + { + // 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) + else if (isReQuery && !suppressQueryExecution) { // When we are re-querying, we should not delay the query await QueryAsync(false, isReQuery: true); @@ -681,6 +691,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 { @@ -690,6 +702,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()) { @@ -701,11 +721,31 @@ 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 (Results.Results.Contains(_lastSelectedResultItem)) + { + Results.SelectedItem = _lastSelectedResultItem; + Results.SelectedIndex = Results.Results.IndexOf(_lastSelectedResultItem); + } + else if (_lastSelectedResultIndex >= 0 && _lastSelectedResultIndex < Results.Results.Count) + { + Results.SelectedIndex = _lastSelectedResultIndex; + Results.SelectedItem = Results.Results[_lastSelectedResultIndex]; + } } else { @@ -1055,6 +1095,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); @@ -1620,67 +1662,27 @@ public void Save() /// public void UpdateResultView(ICollection resultsForUpdates) { - if (!resultsForUpdates.Any()) + if (resultsForUpdates == null || !resultsForUpdates.Any()) + return; + + // Block the query execution when Open ContextMenu is called + if (!QueryResultsSelected()) return; - CancellationToken token; + 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..f6d63f75458 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,16 @@ public void KeepResultsExcept(PluginMetadata metadata) Results.Update(Results.Where(r => r.Result.PluginID != metadata.ID).ToList()); } + public void ResetSelectedIndex() + { + _lastSelectedIndex = 0; + if (Results.Any()) + { + SelectedIndex = 0; + SelectedItem = Results[0]; + } + } + /// /// To avoid deadlock, this method should not called from main thread /// @@ -184,36 +197,88 @@ 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 + if (SelectedItem != null) + { + _lastSelectedItem = SelectedItem; + _lastSelectedIndex = SelectedIndex; + } + + // 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; + } + } + } + + // If no item is selected, select the first one + if (Results.Count > 0 && (SelectedIndex == -1 || SelectedItem == null)) + { + SelectedIndex = 0; + SelectedItem = Results[0]; } - switch (Visibility) + // Visibility update - fix for related issue + if (Results.Count > 0) { - case Visibility.Collapsed when 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; } } + private List NewResults(List newRawResults, string resultId) { if (newRawResults.Count == 0) From 675581fe50e82ea5d31ce9c7149dbe38c9a7bec8 Mon Sep 17 00:00:00 2001 From: DB p Date: Mon, 21 Apr 2025 21:38:18 +0900 Subject: [PATCH 2/7] Add reset --- Flow.Launcher/MainWindow.xaml.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index bf7a45b1d25..d1ff4be152b 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1125,6 +1125,13 @@ private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) { var textBox = (TextBox)sender; _viewModel.QueryText = textBox.Text; + + // Reset Index when query null + if (string.IsNullOrEmpty(textBox.Text)) + { + _viewModel.Results.ResetSelectedIndex(); + } + _viewModel.Query(_settings.SearchQueryResultsWithDelay); } From cc8b9e687e1efafafc84fd83daba4060b26fc58f Mon Sep 17 00:00:00 2001 From: DB P Date: Tue, 22 Apr 2025 10:12:00 +0900 Subject: [PATCH 3/7] Reset selected item and index to prevent stale selections --- Flow.Launcher/ViewModel/ResultsViewModel.cs | 23 ++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index f6d63f75458..6de22e24a69 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -176,11 +176,14 @@ public void KeepResultsExcept(PluginMetadata metadata) public void ResetSelectedIndex() { _lastSelectedIndex = 0; + _lastSelectedItem = null; // prevent accidental reselection of stale item if (Results.Any()) { SelectedIndex = 0; - SelectedItem = Results[0]; + SelectedItem = Results[0]; } + OnPropertyChanged(nameof(SelectedIndex)); + OnPropertyChanged(nameof(SelectedItem)); } /// @@ -201,11 +204,21 @@ public void AddResults(ICollection resultsForUpdates, Cancella return; // Save the currently selected item - if (SelectedItem != null) + ResultViewModel lastSelectedItem = null; + int lastSelectedIndex = -1; + + Application.Current.Dispatcher.Invoke(() => { - _lastSelectedItem = SelectedItem; - _lastSelectedIndex = SelectedIndex; - } + if (SelectedItem != null) + { + lastSelectedItem = SelectedItem; + lastSelectedIndex = SelectedIndex; + } + }); + + // 캡처한 값 저장 + _lastSelectedItem = lastSelectedItem; + _lastSelectedIndex = lastSelectedIndex; // Generate new results var newResults = NewResults(resultsForUpdates); From 4d6dba416b06db99b65c853b59b7e6899754838c Mon Sep 17 00:00:00 2001 From: DB P Date: Tue, 22 Apr 2025 10:15:10 +0900 Subject: [PATCH 4/7] Code Cleanup --- Flow.Launcher/ViewModel/ResultsViewModel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 6de22e24a69..335108958a8 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -215,8 +215,7 @@ public void AddResults(ICollection resultsForUpdates, Cancella lastSelectedIndex = SelectedIndex; } }); - - // 캡처한 값 저장 + _lastSelectedItem = lastSelectedItem; _lastSelectedIndex = lastSelectedIndex; From b544f204643c0d1c91bd9586ba04993a397a571e Mon Sep 17 00:00:00 2001 From: DB p Date: Tue, 22 Apr 2025 10:46:30 +0900 Subject: [PATCH 5/7] Refactor query text handling to reset selection when query is empty --- Flow.Launcher/MainWindow.xaml.cs | 11 +++-------- Flow.Launcher/ViewModel/MainViewModel.cs | 20 +++++++++----------- Flow.Launcher/ViewModel/ResultsViewModel.cs | 8 ++++---- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index d1ff4be152b..91dc6332546 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1124,14 +1124,9 @@ private void SetupResizeMode() private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) { var textBox = (TextBox)sender; - _viewModel.QueryText = textBox.Text; - - // Reset Index when query null - if (string.IsNullOrEmpty(textBox.Text)) - { - _viewModel.Results.ResetSelectedIndex(); - } - + 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 d1aea3bf995..4cc13c885e1 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -659,21 +659,11 @@ private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false { // Change query text first QueryText = queryText; - - if (string.IsNullOrEmpty(queryText)) - { - Results.ResetSelectedIndex(); - } - - // Check if we are in the process of changing query text + ResetSelectionIfQueryEmpty(queryText); if (!suppressQueryExecution) { - // 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 && !suppressQueryExecution) @@ -1350,6 +1340,14 @@ async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) } } } + + public void ResetSelectionIfQueryEmpty(string queryText) + { + if (string.IsNullOrEmpty(queryText)) + { + Results.ResetSelectedIndex(); + } + } private Query ConstructQuery(string queryText, IEnumerable customShortcuts, IEnumerable builtInShortcuts) diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 335108958a8..e8b00aeba64 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -229,14 +229,13 @@ public void AddResults(ICollection resultsForUpdates, Cancella UpdateResults(newResults, reselect, token); } - private void UpdateResults(List newResults, bool reselect = true, + private void UpdateResults(List newResults, bool reselect = true, CancellationToken token = default) { lock (_collectionLock) { // Update previous results and UI Results.Update(newResults, token); - // Only perform selection logic if reselect is true if (reselect && Results.Any()) { @@ -267,7 +266,6 @@ private void UpdateResults(List newResults, bool reselect = tru SelectedIndex = 0; SelectedItem = Results[0]; } - // Visibility update - fix for related issue if (Results.Count > 0) { @@ -277,7 +275,6 @@ private void UpdateResults(List newResults, bool reselect = tru SelectedIndex = 0; SelectedItem = Results[0]; } - // 2. Update visibility if (Visibility == Visibility.Collapsed) { @@ -288,6 +285,9 @@ private void UpdateResults(List newResults, bool reselect = tru { Visibility = Visibility.Collapsed; } + // Notify property changes to update UI bindings + OnPropertyChanged(nameof(SelectedIndex)); + OnPropertyChanged(nameof(SelectedItem)); } From f3ded2190d661161c6c34999177afcad10755772 Mon Sep 17 00:00:00 2001 From: DB p Date: Wed, 23 Apr 2025 10:52:40 +0900 Subject: [PATCH 6/7] Refactor selected item restoration logic to simplify index checks --- Flow.Launcher/ViewModel/MainViewModel.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 4cc13c885e1..0bca61597ab 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -726,12 +726,7 @@ private ResultsViewModel SelectedResults _blockQueryExecution = false; } // restore selected item - if (Results.Results.Contains(_lastSelectedResultItem)) - { - Results.SelectedItem = _lastSelectedResultItem; - Results.SelectedIndex = Results.Results.IndexOf(_lastSelectedResultItem); - } - else if (_lastSelectedResultIndex >= 0 && _lastSelectedResultIndex < Results.Results.Count) + if (_lastSelectedResultIndex >= 0 && _lastSelectedResultIndex < Results.Results.Count) { Results.SelectedIndex = _lastSelectedResultIndex; Results.SelectedItem = Results.Results[_lastSelectedResultIndex]; From a97561e762c14386e500d5027b856176b069c0a3 Mon Sep 17 00:00:00 2001 From: DB p Date: Wed, 23 Apr 2025 10:58:38 +0900 Subject: [PATCH 7/7] Remove redundant assignment of SelectedItem in restoration logic --- Flow.Launcher/ViewModel/MainViewModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 0bca61597ab..6ee792e902f 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -729,7 +729,6 @@ private ResultsViewModel SelectedResults if (_lastSelectedResultIndex >= 0 && _lastSelectedResultIndex < Results.Results.Count) { Results.SelectedIndex = _lastSelectedResultIndex; - Results.SelectedItem = Results.Results[_lastSelectedResultIndex]; } } else