diff --git a/ArcExplorer/Tools/FileTree.cs b/ArcExplorer/Tools/FileTree.cs index b3756c9..5b96813 100644 --- a/ArcExplorer/Tools/FileTree.cs +++ b/ArcExplorer/Tools/FileTree.cs @@ -220,6 +220,7 @@ private static FileNode CreateFileNode(ArcFile arcFile, ArcFileNode arcNode, Act // Lazy initialize the shared file list for performance reasons. List getSharedFiles() => arcFile.GetSharedFilePaths(arcNode, ApplicationSettings.Instance.ArcRegion); + // TODO: Avoid caching this information since it may change when changing regions. var fileNode = new FileNode(arcNode.FileName, arcNode.Path, arcNode.Extension, arcNode.IsShared, arcNode.IsRegional, arcNode.Offset, arcNode.CompSize, arcNode.DecompSize, arcNode.IsCompressed, getSharedFiles); diff --git a/ArcExplorer/ViewModels/MainWindowViewModel.cs b/ArcExplorer/ViewModels/MainWindowViewModel.cs index 062b28b..fdbee5a 100644 --- a/ArcExplorer/ViewModels/MainWindowViewModel.cs +++ b/ArcExplorer/ViewModels/MainWindowViewModel.cs @@ -9,6 +9,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading; using System.Threading.Tasks; namespace ArcExplorer.ViewModels @@ -25,6 +28,8 @@ public AvaloniaList Files } private AvaloniaList files = new AvaloniaList(); + private readonly object searchLock = new object(); + public static Dictionary DescriptionByRegion { get; } = new Dictionary { { Region.Japanese, "Japanese" }, @@ -177,11 +182,7 @@ public string ErrorDescription public string SearchText { get => searchText; - set - { - this.RaiseAndSetIfChanged(ref searchText, value); - SearchArcFile(searchText); - } + set => this.RaiseAndSetIfChanged(ref searchText, value); } private string searchText = ""; @@ -201,6 +202,14 @@ public MainWindowViewModel() { Serilog.Log.Logger.Error("Failed to open Hashes file {@path}", hashesFile); } + + // Throttle search calls to reduce the chance of searching while the user is still typing. + // Subscribing to an asynchronous method described here: + // https://stackoverflow.com/questions/27618401/what-is-the-best-way-to-call-async-methods-using-reactiveui-throttle + this.WhenAnyValue(x => x.SearchText) + .Throttle(TimeSpan.FromMilliseconds(500)) + .SelectMany(SearchArcFileAsync) + .Subscribe(); } private void LogEventHandled(object? sender, EventArgs e) @@ -257,19 +266,31 @@ private void InitializeArcFile(string arcPathText) LoadRootNodes(arcFile); } - private void SearchArcFile(string searchText) + private async Task SearchArcFileAsync(string searchText, CancellationToken cancel) { - if (arcFile == null) - return; + await Task.Run(() => SearchArcFileThreaded(searchText)); + return Unit.Default; + } - if (!string.IsNullOrEmpty(searchText)) - { - var nodes = FileTree.SearchAllNodes(arcFile, BackgroundTaskStart, BackgroundTaskReportProgress, BackgroundTaskEnd, searchText, ApplicationSettings.Instance.MergeTrailingSlash); - Files = new AvaloniaList(nodes.Select(n => new FileGridItem(n))); - } - else + private void SearchArcFileThreaded(string searchText) + { + // This doesn't make all accesses to the file tree thread safe, but it does prevent multiple searches from running at once. + // This is good enough for calling just this method from multiple threads. + // Calling ARC methods should be inherently thread safe since we don't mutate the ARC struct or its wrapper type. + lock (searchLock) { - LoadRootNodes(arcFile); + if (arcFile == null) + return; + + if (!string.IsNullOrEmpty(searchText)) + { + var nodes = FileTree.SearchAllNodes(arcFile, BackgroundTaskStart, BackgroundTaskReportProgress, BackgroundTaskEnd, searchText, ApplicationSettings.Instance.MergeTrailingSlash); + Files = new AvaloniaList(nodes.Select(n => new FileGridItem(n))); + } + else + { + LoadRootNodes(arcFile); + } } } @@ -327,7 +348,7 @@ public void ExitFolder() } // Go up one level in the file tree. - var parent = FileTree.CreateNodeFromPath(arcFile, parentPath, + var parent = FileTree.CreateNodeFromPath(arcFile, parentPath, BackgroundTaskStart, BackgroundTaskReportProgress, BackgroundTaskEnd, ApplicationSettings.Instance.MergeTrailingSlash); if (parent is FolderNode folder) LoadFolder(folder); @@ -342,7 +363,7 @@ private void LoadFolder(string? path) { if (arcFile != null && path != null) { - var parent = FileTree.CreateNodeFromPath(arcFile, path, + var parent = FileTree.CreateNodeFromPath(arcFile, path, BackgroundTaskStart, BackgroundTaskReportProgress, BackgroundTaskEnd, ApplicationSettings.Instance.MergeTrailingSlash); LoadFolder(parent as FolderNode); }