diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index a36668fc40e..68a63667d86 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -84,6 +84,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml index ba4c9e9a451..7d01fdb1c1d 100644 --- a/Flow.Launcher/ResultListBox.xaml +++ b/Flow.Launcher/ResultListBox.xaml @@ -13,7 +13,7 @@ d:DesignWidth="100" Focusable="False" IsSynchronizedWithCurrentItem="True" - ItemsSource="{Binding Results}" + ItemsSource="{Binding Display}" KeyboardNavigation.DirectionalNavigation="Cycle" PreviewMouseDown="ListBox_PreviewMouseDown" PreviewMouseLeftButtonDown="ResultList_PreviewMouseLeftButtonDown" @@ -26,7 +26,7 @@ SelectionMode="Single" Style="{DynamicResource BaseListboxStyle}" VirtualizingStackPanel.IsVirtualizing="True" - VirtualizingStackPanel.VirtualizationMode="Standard" + VirtualizingStackPanel.VirtualizationMode="Recycling" Visibility="{Binding Visibility}" mc:Ignorable="d"> diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 705f6aad0a1..b29f37bff9d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -173,7 +173,7 @@ private void RegisterResultsUpdatedEvent() var token = e.Token == default ? _updateToken : e.Token; PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, token))) + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, token))) { Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); } @@ -710,7 +710,7 @@ private async void QueryResults() if (query == null) // shortcut expanded { Results.Clear(); - Results.Visibility = Visibility.Collapsed; + // Results.Visibility = Visibility.Collapsed; PluginIconPath = null; SearchIconVisibility = Visibility.Visible; return; @@ -814,7 +814,7 @@ async Task QueryTask(PluginPair plugin) results ??= _emptyResult; - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken))) + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, currentCancellationToken))) { Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); } @@ -1042,7 +1042,7 @@ public void Save() /// /// To avoid deadlock, this method should not called from main thread /// - public void UpdateResultView(IEnumerable resultsForUpdates) + public void UpdateResultView(ICollection resultsForUpdates) { if (!resultsForUpdates.Any()) return; diff --git a/Flow.Launcher/ViewModel/ResultsForUpdate.cs b/Flow.Launcher/ViewModel/ResultsForUpdate.cs index 94c6a923aa5..623b78250a3 100644 --- a/Flow.Launcher/ViewModel/ResultsForUpdate.cs +++ b/Flow.Launcher/ViewModel/ResultsForUpdate.cs @@ -13,14 +13,12 @@ public struct ResultsForUpdate public PluginMetadata Metadata { get; } public string ID { get; } - public Query Query { get; } public CancellationToken Token { get; } - public ResultsForUpdate(IReadOnlyList results, PluginMetadata metadata, Query query, CancellationToken token) + public ResultsForUpdate(IReadOnlyList results, PluginMetadata metadata, CancellationToken token) { Results = results; Metadata = metadata; - Query = query; Token = token; ID = metadata.ID; } diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index bb07ce085e8..182f7e564b5 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -1,14 +1,18 @@ -using Flow.Launcher.Infrastructure.UserSettings; +using System; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using System.Collections.Generic; -using System.Collections.Specialized; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive.Linq; using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; +using DynamicData; +using DynamicData.Binding; namespace Flow.Launcher.ViewModel { @@ -16,21 +20,34 @@ public class ResultsViewModel : BaseModel { #region Private Fields - public ResultCollection Results { get; } + private SourceCache ResultCache { get; } = new(r => r.ID); - private readonly object _collectionLock = new object(); + private ReadOnlyObservableCollection display; + public ReadOnlyObservableCollection Display => display; + + + private readonly object _collectionLock = new(); private readonly Settings _settings; private int MaxResults => _settings?.MaxResultsToShow ?? 6; - public ResultsViewModel() + private IDisposable _resultsSubscription; + + private ResultsViewModel() { - Results = new ResultCollection(); - BindingOperations.EnableCollectionSynchronization(Results, _collectionLock); + _resultsSubscription = ResultCache.Connect() + .TransformMany(list => list.Results.Select(r => new ResultViewModel(r, _settings)), + r => r.GetHashCode()) + .Sort(SortExpressionComparer.Descending(r => r.Result.Score)) + .Bind(out display) + .DisposeMany() + .Subscribe(); + + BindingOperations.EnableCollectionSynchronization(Display, _collectionLock); } public ResultsViewModel(Settings settings) : this() { _settings = settings; - _settings.PropertyChanged += (s, e) => + _settings.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(_settings.MaxResultsToShow)) { @@ -48,9 +65,8 @@ public ResultsViewModel(Settings settings) : this() public int SelectedIndex { get; set; } public ResultViewModel SelectedItem { get; set; } - public Thickness Margin { get; set; } - public Visibility Visibility { get; set; } = Visibility.Collapsed; - + public Visibility Visibility { get; set; } = Visibility.Visible; + public ICommand RightClickResultCommand { get; init; } public ICommand LeftClickResultCommand { get; init; } @@ -58,36 +74,18 @@ public ResultsViewModel(Settings settings) : this() #region Private Methods - private int InsertIndexOf(int newScore, IList list) - { - int index = 0; - for (; index < list.Count; index++) - { - var result = list[index]; - if (newScore > result.Result.Score) - { - break; - } - } - return index; - } - private int NewIndex(int i) { - var n = Results.Count; + var n = Display.Count; if (n > 0) { i = (n + i) % n; return i; } - else - { - // SelectedIndex returns -1 if selection is empty. - return -1; - } + // SelectedIndex returns -1 if selection is empty. + return -1; } - #endregion #region Public Methods @@ -120,19 +118,7 @@ public void SelectFirstResult() public void Clear() { lock (_collectionLock) - Results.RemoveAll(); - } - - public void KeepResultsFor(PluginMetadata metadata) - { - lock (_collectionLock) - Results.Update(Results.Where(r => r.Result.PluginID == metadata.ID).ToList()); - } - - public void KeepResultsExcept(PluginMetadata metadata) - { - lock (_collectionLock) - Results.Update(Results.Where(r => r.Result.PluginID != metadata.ID).ToList()); + ResultCache.Clear(); } /// @@ -140,72 +126,55 @@ public void KeepResultsExcept(PluginMetadata metadata) /// public void AddResults(List newRawResults, string resultId) { - var newResults = NewResults(newRawResults, resultId); - - UpdateResults(newResults); + lock (_collectionLock) + { + ResultCache.Edit(list => + { + list.AddOrUpdate(new ResultsForUpdate(newRawResults, new PluginMetadata() + { + ID = resultId + }, default)); + }); + } } + /// /// To avoid deadlock, this method should not called from main thread /// - public void AddResults(IEnumerable resultsForUpdates, CancellationToken token) - { - var newResults = NewResults(resultsForUpdates); - - if (token.IsCancellationRequested) - return; - - UpdateResults(newResults, token); - } - - private void UpdateResults(List newResults, CancellationToken token = default) + public void AddResults(ICollection resultsForUpdates, CancellationToken token) { lock (_collectionLock) { // update UI in one run, so it can avoid UI flickering - Results.Update(newResults, token); - if (Results.Any()) - SelectedItem = Results[0]; + ResultCache.Edit(list => + { + list.AddOrUpdate(resultsForUpdates); + }); + if (display.Any()) + SelectedItem = display[0]; } + // UpdateVisibility(); + } + private void UpdateVisibility() + { + switch (Visibility) { - case Visibility.Collapsed when Results.Count > 0: + case Visibility.Collapsed when Display.Count > 0: SelectedIndex = 0; Visibility = Visibility.Visible; break; - case Visibility.Visible when Results.Count == 0: + case Visibility.Visible when Display.Count == 0: Visibility = Visibility.Collapsed; break; } } - private List NewResults(List newRawResults, string resultId) - { - if (newRawResults.Count == 0) - return Results; - - - var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); - - return Results.Where(r => r.Result.PluginID != resultId) - .Concat(newResults) - .OrderByDescending(r => r.Result.Score) - .ToList(); - } - - private List NewResults(IEnumerable resultsForUpdates) - { - if (!resultsForUpdates.Any()) - return Results; - - return Results.Where(r => r != null && !resultsForUpdates.Any(u => u.ID == r.Result.PluginID)) - .Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) - .OrderByDescending(rv => rv.Result.Score) - .ToList(); - } #endregion #region FormattedText Dependency Property + public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached( "FormattedText", typeof(Inline), @@ -224,8 +193,7 @@ public static Inline GetFormattedText(DependencyObject textBlock) private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - var textBlock = d as TextBlock; - if (textBlock == null) return; + if (d is not TextBlock textBlock) return; var inline = (Inline)e.NewValue; @@ -234,82 +202,8 @@ private static void FormattedTextPropertyChanged(DependencyObject d, DependencyP textBlock.Inlines.Add(inline); } - #endregion - - public class ResultCollection : List, INotifyCollectionChanged - { - private long editTime = 0; - - private CancellationToken _token; - - public event NotifyCollectionChangedEventHandler CollectionChanged; - - - protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) - { - CollectionChanged?.Invoke(this, e); - } - - public void BulkAddAll(List resultViews) - { - AddRange(resultViews); - - // can return because the list will be cleared next time updated, which include a reset event - if (_token.IsCancellationRequested) - return; - - // manually update event - // wpf use DirectX / double buffered already, so just reset all won't cause ui flickering - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - private void AddAll(List Items) - { - for (int i = 0; i < Items.Count; i++) - { - var item = Items[i]; - if (_token.IsCancellationRequested) - return; - Add(item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i)); - } - } - public void RemoveAll(int Capacity = 512) - { - Clear(); - if (this.Capacity > 8000 && Capacity < this.Capacity) - this.Capacity = Capacity; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - /// - /// Update the results collection with new results, try to keep identical results - /// - /// - public void Update(List newItems, CancellationToken token = default) - { - _token = token; - if (Count == 0 && newItems.Count == 0 || _token.IsCancellationRequested) - return; + #endregion - if (editTime < 10 || newItems.Count < 30) - { - if (Count != 0) RemoveAll(newItems.Count); - AddAll(newItems); - editTime++; - return; - } - else - { - Clear(); - BulkAddAll(newItems); - if (Capacity > 8000 && newItems.Count < 3000) - { - Capacity = newItems.Count; - } - editTime++; - } - } - } } }