mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Refactor ResultsViewModel.cs with DynamicData
1. Use DynamicData SourceCache instead of our ResultCollection to reduce code complexity 2. Stop trigger Visibility when changes from empty to not empty or vise versa
This commit is contained in:
parent
c0f46f24de
commit
453e43088c
5 changed files with 75 additions and 182 deletions
|
|
@ -84,6 +84,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
|
||||
<PackageReference Include="DynamicData" Version="7.12.11" />
|
||||
<PackageReference Include="Fody" Version="6.5.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
|
|
|||
|
|
@ -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,8 +26,8 @@
|
|||
SelectionMode="Single"
|
||||
Style="{DynamicResource BaseListboxStyle}"
|
||||
VirtualizingStackPanel.IsVirtualizing="True"
|
||||
VirtualizingStackPanel.VirtualizationMode="Standard"
|
||||
Visibility="{Binding Visbility}"
|
||||
VirtualizingStackPanel.VirtualizationMode="Recycling"
|
||||
Visibility="{Binding Visibility}"
|
||||
mc:Ignorable="d">
|
||||
<!-- IsSynchronizedWithCurrentItem: http://stackoverflow.com/a/7833798/2833083 -->
|
||||
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ namespace Flow.Launcher.ViewModel
|
|||
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");
|
||||
}
|
||||
|
|
@ -535,13 +535,13 @@ namespace Flow.Launcher.ViewModel
|
|||
_selectedResults = value;
|
||||
if (SelectedIsFromQueryResults())
|
||||
{
|
||||
ContextMenu.Visbility = Visibility.Collapsed;
|
||||
History.Visbility = Visibility.Collapsed;
|
||||
ContextMenu.Visibility = Visibility.Collapsed;
|
||||
History.Visibility = Visibility.Collapsed;
|
||||
ChangeQueryText(_queryTextBeforeLeaveResults);
|
||||
}
|
||||
else
|
||||
{
|
||||
Results.Visbility = Visibility.Collapsed;
|
||||
Results.Visibility = Visibility.Collapsed;
|
||||
_queryTextBeforeLeaveResults = QueryText;
|
||||
|
||||
|
||||
|
|
@ -559,7 +559,7 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
_selectedResults.Visbility = Visibility.Visible;
|
||||
_selectedResults.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -710,7 +710,7 @@ namespace Flow.Launcher.ViewModel
|
|||
if (query == null) // shortcut expanded
|
||||
{
|
||||
Results.Clear();
|
||||
Results.Visbility = Visibility.Collapsed;
|
||||
// Results.Visbility = Visibility.Collapsed;
|
||||
PluginIconPath = null;
|
||||
SearchIconVisibility = Visibility.Visible;
|
||||
return;
|
||||
|
|
@ -814,7 +814,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
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");
|
||||
}
|
||||
|
|
@ -1037,7 +1037,7 @@ namespace Flow.Launcher.ViewModel
|
|||
/// <summary>
|
||||
/// To avoid deadlock, this method should not called from main thread
|
||||
/// </summary>
|
||||
public void UpdateResultView(IEnumerable<ResultsForUpdate> resultsForUpdates)
|
||||
public void UpdateResultView(ICollection<ResultsForUpdate> resultsForUpdates)
|
||||
{
|
||||
if (!resultsForUpdates.Any())
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -13,14 +13,12 @@ namespace Flow.Launcher.ViewModel
|
|||
public PluginMetadata Metadata { get; }
|
||||
public string ID { get; }
|
||||
|
||||
public Query Query { get; }
|
||||
public CancellationToken Token { get; }
|
||||
|
||||
public ResultsForUpdate(IReadOnlyList<Result> results, PluginMetadata metadata, Query query, CancellationToken token)
|
||||
public ResultsForUpdate(IReadOnlyList<Result> results, PluginMetadata metadata, CancellationToken token)
|
||||
{
|
||||
Results = results;
|
||||
Metadata = metadata;
|
||||
Query = query;
|
||||
Token = token;
|
||||
ID = metadata.ID;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
#region Private Fields
|
||||
|
||||
public ResultCollection Results { get; }
|
||||
private SourceCache<ResultsForUpdate, string> ResultCache { get; } = new(r => r.ID);
|
||||
|
||||
private readonly object _collectionLock = new object();
|
||||
private ReadOnlyObservableCollection<ResultViewModel> display;
|
||||
public ReadOnlyObservableCollection<ResultViewModel> 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<ResultViewModel>.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 @@ namespace Flow.Launcher.ViewModel
|
|||
public int SelectedIndex { get; set; }
|
||||
|
||||
public ResultViewModel SelectedItem { get; set; }
|
||||
public Thickness Margin { get; set; }
|
||||
public Visibility Visbility { 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 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
#region Private Methods
|
||||
|
||||
private int InsertIndexOf(int newScore, IList<ResultViewModel> 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 @@ namespace Flow.Launcher.ViewModel
|
|||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -140,72 +126,55 @@ namespace Flow.Launcher.ViewModel
|
|||
/// </summary>
|
||||
public void AddResults(List<Result> 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To avoid deadlock, this method should not called from main thread
|
||||
/// </summary>
|
||||
public void AddResults(IEnumerable<ResultsForUpdate> resultsForUpdates, CancellationToken token)
|
||||
{
|
||||
var newResults = NewResults(resultsForUpdates);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
UpdateResults(newResults, token);
|
||||
}
|
||||
|
||||
private void UpdateResults(List<ResultViewModel> newResults, CancellationToken token = default)
|
||||
public void AddResults(ICollection<ResultsForUpdate> 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];
|
||||
}
|
||||
|
||||
switch (Visbility)
|
||||
// UpdateVisibility();
|
||||
}
|
||||
private void UpdateVisibility()
|
||||
{
|
||||
|
||||
switch (Visibility)
|
||||
{
|
||||
case Visibility.Collapsed when Results.Count > 0:
|
||||
case Visibility.Collapsed when Display.Count > 0:
|
||||
SelectedIndex = 0;
|
||||
Visbility = Visibility.Visible;
|
||||
Visibility = Visibility.Visible;
|
||||
break;
|
||||
case Visibility.Visible when Results.Count == 0:
|
||||
Visbility = Visibility.Collapsed;
|
||||
case Visibility.Visible when Display.Count == 0:
|
||||
Visibility = Visibility.Collapsed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private List<ResultViewModel> NewResults(List<Result> 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<ResultViewModel> NewResults(IEnumerable<ResultsForUpdate> 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 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
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 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
textBlock.Inlines.Add(inline);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public class ResultCollection : List<ResultViewModel>, 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<ResultViewModel> 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<ResultViewModel> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the results collection with new results, try to keep identical results
|
||||
/// </summary>
|
||||
/// <param name="newItems"></param>
|
||||
public void Update(List<ResultViewModel> newItems, CancellationToken token = default)
|
||||
{
|
||||
_token = token;
|
||||
if (Count == 0 && newItems.Count == 0 || _token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue