99using System ;
1010using System . Collections . Generic ;
1111using System . Linq ;
12+ using System . Reactive ;
13+ using System . Reactive . Linq ;
14+ using System . Threading ;
1215using System . Threading . Tasks ;
1316
1417namespace ArcExplorer . ViewModels
@@ -25,6 +28,8 @@ public AvaloniaList<FileGridItem> Files
2528 }
2629 private AvaloniaList < FileGridItem > files = new AvaloniaList < FileGridItem > ( ) ;
2730
31+ private readonly object searchLock = new object ( ) ;
32+
2833 public static Dictionary < Region , string > DescriptionByRegion { get ; } = new Dictionary < Region , string >
2934 {
3035 { Region . Japanese , "Japanese" } ,
@@ -177,11 +182,7 @@ public string ErrorDescription
177182 public string SearchText
178183 {
179184 get => searchText ;
180- set
181- {
182- this . RaiseAndSetIfChanged ( ref searchText , value ) ;
183- SearchArcFile ( searchText ) ;
184- }
185+ set => this . RaiseAndSetIfChanged ( ref searchText , value ) ;
185186 }
186187 private string searchText = "" ;
187188
@@ -201,6 +202,14 @@ public MainWindowViewModel()
201202 {
202203 Serilog . Log . Logger . Error ( "Failed to open Hashes file {@path}" , hashesFile ) ;
203204 }
205+
206+ // Throttle search calls to reduce the chance of searching while the user is still typing.
207+ // Subscribing to an asynchronous method described here:
208+ // https://stackoverflow.com/questions/27618401/what-is-the-best-way-to-call-async-methods-using-reactiveui-throttle
209+ this . WhenAnyValue ( x => x . SearchText )
210+ . Throttle ( TimeSpan . FromMilliseconds ( 500 ) )
211+ . SelectMany ( SearchArcFileAsync )
212+ . Subscribe ( ) ;
204213 }
205214
206215 private void LogEventHandled ( object ? sender , EventArgs e )
@@ -257,19 +266,31 @@ private void InitializeArcFile(string arcPathText)
257266 LoadRootNodes ( arcFile ) ;
258267 }
259268
260- private void SearchArcFile ( string searchText )
269+ private async Task < Unit > SearchArcFileAsync ( string searchText , CancellationToken cancel )
261270 {
262- if ( arcFile == null )
263- return ;
271+ await Task . Run ( ( ) => SearchArcFileThreaded ( searchText ) ) ;
272+ return Unit . Default ;
273+ }
264274
265- if ( ! string . IsNullOrEmpty ( searchText ) )
266- {
267- var nodes = FileTree . SearchAllNodes ( arcFile , BackgroundTaskStart , BackgroundTaskReportProgress , BackgroundTaskEnd , searchText , ApplicationSettings . Instance . MergeTrailingSlash ) ;
268- Files = new AvaloniaList < FileGridItem > ( nodes . Select ( n => new FileGridItem ( n ) ) ) ;
269- }
270- else
275+ private void SearchArcFileThreaded ( string searchText )
276+ {
277+ // This doesn't make all accesses to the file tree thread safe, but it does prevent multiple searches from running at once.
278+ // This is good enough for calling just this method from multiple threads.
279+ // Calling ARC methods should be inherently thread safe since we don't mutate the ARC struct or its wrapper type.
280+ lock ( searchLock )
271281 {
272- LoadRootNodes ( arcFile ) ;
282+ if ( arcFile == null )
283+ return ;
284+
285+ if ( ! string . IsNullOrEmpty ( searchText ) )
286+ {
287+ var nodes = FileTree . SearchAllNodes ( arcFile , BackgroundTaskStart , BackgroundTaskReportProgress , BackgroundTaskEnd , searchText , ApplicationSettings . Instance . MergeTrailingSlash ) ;
288+ Files = new AvaloniaList < FileGridItem > ( nodes . Select ( n => new FileGridItem ( n ) ) ) ;
289+ }
290+ else
291+ {
292+ LoadRootNodes ( arcFile ) ;
293+ }
273294 }
274295 }
275296
@@ -327,7 +348,7 @@ public void ExitFolder()
327348 }
328349
329350 // Go up one level in the file tree.
330- var parent = FileTree . CreateNodeFromPath ( arcFile , parentPath ,
351+ var parent = FileTree . CreateNodeFromPath ( arcFile , parentPath ,
331352 BackgroundTaskStart , BackgroundTaskReportProgress , BackgroundTaskEnd , ApplicationSettings . Instance . MergeTrailingSlash ) ;
332353 if ( parent is FolderNode folder )
333354 LoadFolder ( folder ) ;
@@ -342,7 +363,7 @@ private void LoadFolder(string? path)
342363 {
343364 if ( arcFile != null && path != null )
344365 {
345- var parent = FileTree . CreateNodeFromPath ( arcFile , path ,
366+ var parent = FileTree . CreateNodeFromPath ( arcFile , path ,
346367 BackgroundTaskStart , BackgroundTaskReportProgress , BackgroundTaskEnd , ApplicationSettings . Instance . MergeTrailingSlash ) ;
347368 LoadFolder ( parent as FolderNode ) ;
348369 }
0 commit comments