Add debounced package search

This commit is contained in:
Marlon 2026-07-16 15:27:27 +02:00
parent 07ede1555e
commit 990c38bbcb
No known key found for this signature in database
GPG Key ID: 5C65CA190C38792F
3 changed files with 80 additions and 12 deletions

View File

@ -68,6 +68,10 @@ public class SearchViewModel : ViewModel
private set => SetProperty(ref _searchResults, value);
}
private ListCollectionView _searchResultsView;
private string[] _filters = [];
private Regex _filterRegex;
private bool _isRegexValid = true;
public ListCollectionView SearchResultsView
{
get
@ -75,9 +79,10 @@ public class SearchViewModel : ViewModel
if (_searchResultsView != null)
return _searchResultsView;
PrepareFilter();
_searchResultsView = new ListCollectionView(SearchResults)
{
Filter = e => ItemFilter(e, FilterText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries)),
Filter = ItemFilter,
};
ResultsCount = _searchResultsView.Count;
return _searchResultsView;
@ -91,6 +96,7 @@ public class SearchViewModel : ViewModel
public void RefreshFilter()
{
PrepareFilter();
SearchResultsView.Refresh();
ResultsCount = SearchResultsView.Count;
}
@ -148,16 +154,38 @@ public class SearchViewModel : ViewModel
ChangeCollection(sorted, RefFile);
}
private bool ItemFilter(object item, IEnumerable<string> filters)
private void PrepareFilter()
{
_filters = FilterText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
_filterRegex = null;
_isRegexValid = true;
if (!HasRegexEnabled)
return;
var options = RegexOptions.Compiled;
if (!HasMatchCaseEnabled)
options |= RegexOptions.IgnoreCase;
try
{
_filterRegex = new Regex(FilterText, options);
}
catch (ArgumentException)
{
_isRegexValid = false;
}
}
private bool ItemFilter(object item)
{
if (item is not GameFile entry)
return true;
if (!HasRegexEnabled)
return filters.All(x => entry.Path.Contains(x, HasMatchCaseEnabled ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));
return _filters.All(x => entry.Path.Contains(x,
HasMatchCaseEnabled ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));
var o = RegexOptions.None;
if (!HasMatchCaseEnabled) o |= RegexOptions.IgnoreCase;
return new Regex(FilterText, o).Match(entry.Path).Success;
return _isRegexValid && _filterRegex.IsMatch(entry.Path);
}
}

View File

@ -7,6 +7,7 @@
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" ResizeMode="CanResize" ShowInTaskbar="True"
IconVisibility="Collapsed" KeyDown="OnWindowKeyDown" StateChanged="OnStateChanged"
Closed="OnWindowClosed"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.75'}"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.65'}">
<adonisControls:AdonisWindow.Style>
@ -38,14 +39,15 @@
</Canvas>
</Viewbox>
</Grid>
<TextBox x:Name="SearchTextBox" Grid.Column="0" Grid.ColumnSpan="2" Padding="25 0 0 0" AcceptsTab="False" AcceptsReturn="False">
<TextBox x:Name="SearchTextBox" Grid.Column="0" Grid.ColumnSpan="2" Padding="25 0 0 0" AcceptsTab="False" AcceptsReturn="False"
TextChanged="OnSearchTextChanged">
<TextBox.Style>
<Style TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Text" Value="{Binding SearchTab.FilterText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="Write your pattern and press enter..." />
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="Search package paths..." />
<Style.Triggers>
<DataTrigger Binding="{Binding SearchTab.HasRegexEnabled}" Value="True">
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="Write your regex pattern and press enter..." />
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="Search package paths with a regex..." />
</DataTrigger>
</Style.Triggers>
</Style>
@ -402,14 +404,15 @@
</Canvas>
</Viewbox>
</Grid>
<TextBox x:Name="RefSearchTextBox" Grid.Column="0" Grid.ColumnSpan="2" Padding="25 0 0 0" AcceptsTab="False" AcceptsReturn="False">
<TextBox x:Name="RefSearchTextBox" Grid.Column="0" Grid.ColumnSpan="2" Padding="25 0 0 0" AcceptsTab="False" AcceptsReturn="False"
TextChanged="OnSearchTextChanged">
<TextBox.Style>
<Style TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Text" Value="{Binding RefTab.FilterText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="Write your pattern and press enter..." />
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="Search package paths..." />
<Style.Triggers>
<DataTrigger Binding="{Binding RefTab.HasRegexEnabled}" Value="True">
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="Write your regex pattern and press enter..." />
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="Search package paths with a regex..." />
</DataTrigger>
</Style.Triggers>
</Style>

View File

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using CUE4Parse.FileProvider.Objects;
using FModel.Services;
using FModel.ViewModels;
@ -19,15 +20,22 @@ public enum ESearchViewTab
public partial class SearchView
{
private static readonly TimeSpan AutoSearchDelay = TimeSpan.FromMilliseconds(500);
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
private SearchViewModel _searchViewModel => _applicationView.CUE4Parse.SearchVm;
private SearchViewModel _refViewModel => _applicationView.CUE4Parse.RefVm;
private ESearchViewTab _currentTab = ESearchViewTab.SearchView;
private readonly DispatcherTimer _autoSearchTimer;
private SearchViewModel _pendingAutoSearch;
public SearchView()
{
_autoSearchTimer = new DispatcherTimer { Interval = AutoSearchDelay };
_autoSearchTimer.Tick += OnAutoSearchTimerTick;
DataContext = new
{
mainApplication = _applicationView,
@ -95,9 +103,30 @@ public partial class SearchView
if (viewModel == null)
return;
viewModel.FilterText = string.Empty;
CancelAutoSearch();
viewModel.RefreshFilter();
}
private void OnSearchTextChanged(object sender, TextChangedEventArgs e)
{
_pendingAutoSearch = ReferenceEquals(sender, RefSearchTextBox) ? _refViewModel : _searchViewModel;
_autoSearchTimer.Stop();
_autoSearchTimer.Start();
}
private void OnAutoSearchTimerTick(object sender, EventArgs e)
{
var viewModel = _pendingAutoSearch;
CancelAutoSearch();
viewModel?.RefreshFilter();
}
private void CancelAutoSearch()
{
_autoSearchTimer.Stop();
_pendingAutoSearch = null;
}
private SearchViewModel CurrentViewModel => _currentTab switch
{
ESearchViewTab.SearchView => _applicationView.CUE4Parse.SearchVm,
@ -187,9 +216,17 @@ public partial class SearchView
{
if (e.Key != Key.Enter)
return;
CancelAutoSearch();
CurrentViewModel?.RefreshFilter();
}
private void OnWindowClosed(object sender, EventArgs e)
{
CancelAutoSearch();
_autoSearchTimer.Tick -= OnAutoSearchTimerTick;
}
private void OnStateChanged(object sender, EventArgs e)
{
switch (WindowState)