From 990c38bbcb5cfa6755c5b4f6bd0685e47c6ccad7 Mon Sep 17 00:00:00 2001 From: Marlon Date: Thu, 16 Jul 2026 15:27:27 +0200 Subject: [PATCH] Add debounced package search --- FModel/ViewModels/SearchViewModel.cs | 40 +++++++++++++++++++++++----- FModel/Views/SearchView.xaml | 15 ++++++----- FModel/Views/SearchView.xaml.cs | 37 +++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/FModel/ViewModels/SearchViewModel.cs b/FModel/ViewModels/SearchViewModel.cs index 248548b9..714629c6 100644 --- a/FModel/ViewModels/SearchViewModel.cs +++ b/FModel/ViewModels/SearchViewModel.cs @@ -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 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); } } diff --git a/FModel/Views/SearchView.xaml b/FModel/Views/SearchView.xaml index 9e3a1f93..36a6e09c 100644 --- a/FModel/Views/SearchView.xaml +++ b/FModel/Views/SearchView.xaml @@ -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'}"> @@ -38,14 +39,15 @@ - + @@ -402,14 +404,15 @@ - + diff --git a/FModel/Views/SearchView.xaml.cs b/FModel/Views/SearchView.xaml.cs index aaa15c72..bdc78020 100644 --- a/FModel/Views/SearchView.xaml.cs +++ b/FModel/Views/SearchView.xaml.cs @@ -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)