This commit is contained in:
Marlon 2026-07-16 20:23:16 +00:00 committed by GitHub
commit f9acab6c4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 670 additions and 259 deletions

@ -1 +1 @@
Subproject commit 396ede2a1d89752142c3b73dba7c908467ccf3a3
Subproject commit 25d982ecf40620038525ebd7adbd07ed644df2fe

View File

@ -2,11 +2,14 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace FModel.Framework;
public sealed class RangeObservableCollection<T> : ObservableCollection<T>
{
private static readonly PropertyChangedEventArgs CountChanged = new(nameof(Count));
private static readonly PropertyChangedEventArgs IndexerChanged = new("Item[]");
private bool _suppressNotification;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
@ -20,15 +23,26 @@ public sealed class RangeObservableCollection<T> : ObservableCollection<T>
if (list == null)
throw new ArgumentNullException(nameof(list));
_suppressNotification = true;
var changed = false;
foreach (var item in list)
Add(item);
{
Items.Add(item);
changed = true;
}
_suppressNotification = false;
if (!changed || _suppressNotification)
return;
OnPropertyChanged(CountChanged);
OnPropertyChanged(IndexerChanged);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>
/// Adds an item while constructing a collection that has not been published to a binding yet.
/// </summary>
public void AddWithoutNotification(T item) => Items.Add(item);
public void SetSuppressionState(bool state)
{
_suppressNotification = state;
@ -38,4 +52,4 @@ public sealed class RangeObservableCollection<T> : ObservableCollection<T>
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(changedAction));
}
}
}

View File

@ -316,7 +316,7 @@
<MultiBinding StringFormat="{}'{0}' has {1} folders and {2} packages">
<Binding Path="SelectedItem.Header" ElementName="AssetsFolderName" FallbackValue="None" />
<Binding Path="SelectedItem.FoldersView.Count" ElementName="AssetsFolderName" FallbackValue="0" />
<Binding Path="SelectedItem.AssetsList.Assets.Count" ElementName="AssetsFolderName" FallbackValue="0" />
<Binding Path="SelectedItem.AssetsList.Count" ElementName="AssetsFolderName" FallbackValue="0" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
@ -371,7 +371,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding SelectedItem.AssetsList.Assets.Count, ElementName=AssetsFolderName, FallbackValue=0}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding SelectedItem.AssetsList.Count, ElementName=AssetsFolderName, FallbackValue=0}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="Packages Count" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding SelectedItem.FoldersView.Count, ElementName=AssetsFolderName, FallbackValue=0}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="Folders Count" VerticalAlignment="Center" HorizontalAlignment="Right" />
@ -386,7 +386,7 @@
</Grid>
</TabItem>
<TabItem Style="{StaticResource TabItemFillSpace}"
Header="{Binding SelectedItem.AssetsList.Assets.Count, FallbackValue=0, ElementName=AssetsFolderName}"
Header="{Binding SelectedItem.AssetsList.Count, FallbackValue=0, ElementName=AssetsFolderName}"
HeaderStringFormat="{}{0} Packages">
<DockPanel>

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
@ -7,6 +8,8 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels;
@ -106,6 +109,14 @@ public partial class MainWindow
ApplicationViewModel.InitZlib()
);
var auxiliaryInitialization = Task.WhenAll(
_applicationView.CUE4Parse.InitMappings(),
ApplicationViewModel.InitDetex(),
ApplicationViewModel.InitVgmStream(),
ApplicationViewModel.InitImGuiSettings(newOrUpdated),
UserSettings.Default.DecompileLua ? ApplicationViewModel.InitUnluac() : Task.CompletedTask
);
await _applicationView.CUE4Parse.Initialize();
await _applicationView.AesManager.InitAes();
await _applicationView.UpdateProvider(true);
@ -116,16 +127,12 @@ public partial class MainWindow
await Task.WhenAll(
_applicationView.CUE4Parse.VerifyConsoleVariables(),
_applicationView.CUE4Parse.VerifyOnDemandArchives(),
_applicationView.CUE4Parse.InitMappings(),
ApplicationViewModel.InitDetex(),
ApplicationViewModel.InitVgmStream(),
ApplicationViewModel.InitImGuiSettings(newOrUpdated),
auxiliaryInitialization,
Task.Run(() =>
{
if (UserSettings.Default.DiscordRpc == EDiscordRpc.Always)
_discordHandler.Initialize(_applicationView.GameDisplayName);
}),
UserSettings.Default.DecompileLua ? ApplicationViewModel.InitUnluac() : Task.CompletedTask
})
).ConfigureAwait(false);
#if DEBUG
@ -254,6 +261,9 @@ public partial class MainWindow
private void OnPreviewTexturesToggled(object sender, RoutedEventArgs e) => ItemContainerGenerator_StatusChanged(AssetsExplorer.ItemContainerGenerator, EventArgs.Empty);
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (_applicationView.IsAssetPreviewLoadingSuspended)
return;
if (sender is not ItemContainerGenerator { Status: GeneratorStatus.ContainersGenerated } generator)
return;
@ -277,6 +287,94 @@ public partial class MainWindow
}
}
public void RefreshVisibleAssetPreviews()
=> ItemContainerGenerator_StatusChanged(AssetsExplorer.ItemContainerGenerator, EventArgs.Empty);
public async Task<bool> SelectFolderAsync(IReadOnlyList<TreeItem> path)
{
if (path.Count == 0)
return false;
// Set the complete model path first. A virtualized container will pick up the
// expansion state whenever WPF realizes it, independently of UI timing.
for (var i = 0; i < path.Count - 1; i++)
path[i].IsExpanded = true;
ItemsControl parent = AssetsFolderName;
TreeViewItem container = null;
for (var i = 0; i < path.Count; i++)
{
container = await GetTreeViewItemAsync(parent, path[i]);
if (container == null)
return false;
// Only ancestors must be expanded. Expanding the target itself can realize a
// large child subtree even though Go To never needs to display those children.
if (i < path.Count - 1)
{
container.IsExpanded = true;
}
parent = container;
}
container.IsSelected = false;
container.IsSelected = true;
await Dispatcher.InvokeAsync(static () => { }, DispatcherPriority.Background);
return ReferenceEquals(AssetsFolderName.SelectedItem, path[^1]);
}
private async Task<TreeViewItem> GetTreeViewItemAsync(ItemsControl parent, TreeItem item)
{
// FoldersView is bound with IsAsync=True. For large sibling collections its sorted
// view can take substantially longer than a fixed number of dispatcher turns.
var timeoutAt = DateTime.UtcNow + TimeSpan.FromSeconds(15);
while (DateTime.UtcNow < timeoutAt)
{
parent.ApplyTemplate();
var presenter = parent.Template.FindName("ItemsHost", parent) as ItemsPresenter ??
FindVisualChild<ItemsPresenter>(parent);
if (presenter == null)
{
parent.UpdateLayout();
presenter = FindVisualChild<ItemsPresenter>(parent);
}
presenter?.ApplyTemplate();
var index = parent.Items.IndexOf(item);
if (index >= 0 && presenter != null && VisualTreeHelper.GetChildrenCount(presenter) > 0 &&
VisualTreeHelper.GetChild(presenter, 0) is NavigableVirtualizingStackPanel panel)
{
_ = panel.Children; // Ensure that the item generator is connected.
panel.BringItemIntoView(index);
parent.UpdateLayout();
if (parent.ItemContainerGenerator.ContainerFromIndex(index) is TreeViewItem container)
return container;
}
await Task.Delay(1);
}
return null;
}
private static T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T match)
return match;
if (FindVisualChild<T>(child) is { } descendant)
return descendant;
}
return null;
}
private void OnAssetsTreeSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (sender is not TreeView { SelectedItem: TreeItem }) return;
@ -338,7 +436,7 @@ public partial class MainWindow
}
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];
@ -360,14 +458,14 @@ public partial class MainWindow
if (e.Key != Key.Enter || sender is not TreeView treeView || treeView.SelectedItem is not TreeItem folder)
return;
if ((folder.IsExpanded || folder.Folders.Count == 0) && folder.AssetsList.Assets.Count > 0)
if ((folder.IsExpanded || folder.Folders.Count == 0) && folder.AssetsList.Count > 0)
{
_applicationView.SelectedLeftTabIndex++;
return;
}
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];

View File

@ -6,6 +6,7 @@ using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using CUE4Parse_Conversion.Textures.BC;
using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
@ -27,6 +28,10 @@ namespace FModel.ViewModels;
public class ApplicationViewModel : ViewModel
{
private readonly object _providerStatusLock = new();
private (string Label, string Prefix)? _pendingProviderStatus;
private bool _providerStatusScheduled;
private EBuildKind _build;
public EBuildKind Build
{
@ -60,6 +65,8 @@ public class ApplicationViewModel : ViewModel
}
}
public bool IsAssetPreviewLoadingSuspended { get; set; }
private int _selectedLeftTabIndex;
public int SelectedLeftTabIndex
{
@ -113,13 +120,13 @@ public class ApplicationViewModel : ViewModel
CUE4Parse.Provider.VfsRegistered += (sender, count) =>
{
if (sender is not IAesVfsReader reader) return;
Status.UpdateStatusLabel($"{count} Archives ({reader.Name})", "Registered");
QueueProviderStatus($"{count} Archives ({reader.Name})", "Registered");
CUE4Parse.GameDirectory.Add(reader);
};
CUE4Parse.Provider.VfsMounted += (sender, count) =>
{
if (sender is not IAesVfsReader reader) return;
Status.UpdateStatusLabel($"{count:N0} Packages ({reader.Name})", "Mounted");
QueueProviderStatus($"{count:N0} Packages ({reader.Name})", "Mounted");
CUE4Parse.GameDirectory.Verify(reader);
};
CUE4Parse.Provider.VfsUnmounted += (sender, _) =>
@ -135,6 +142,34 @@ public class ApplicationViewModel : ViewModel
Status.SetStatus(EStatusKind.Ready);
}
private void QueueProviderStatus(string label, string prefix)
{
lock (_providerStatusLock)
{
_pendingProviderStatus = (label, prefix);
if (_providerStatusScheduled)
return;
_providerStatusScheduled = true;
}
_ = Application.Current.Dispatcher.BeginInvoke(PublishProviderStatus, DispatcherPriority.Background);
}
private void PublishProviderStatus()
{
(string Label, string Prefix)? update;
lock (_providerStatusLock)
{
update = _pendingProviderStatus;
_pendingProviderStatus = null;
_providerStatusScheduled = false;
}
if (update is { } status)
Status.UpdateStatusLabel(status.Label, status.Prefix);
}
public DirectorySettings AvoidEmptyGameDirectory(bool bAlreadyLaunched)
{
var gameDirectory = UserSettings.Default.GameDirectory;

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
@ -16,14 +15,9 @@ using FModel.Services;
namespace FModel.ViewModels;
public class TreeItem : ViewModel
public sealed class TreeItem : ViewModel
{
private readonly string _header;
public string Header
{
get => _header;
private init => SetProperty(ref _header, value);
}
public string Header { get; }
private bool _isExpanded;
public bool IsExpanded
@ -39,26 +33,9 @@ public class TreeItem : ViewModel
set => SetProperty(ref _isSelected, value);
}
private string _archive;
public string Archive
{
get => _archive;
private set => SetProperty(ref _archive, value);
}
private string _mountPoint;
public string MountPoint
{
get => _mountPoint;
private set => SetProperty(ref _mountPoint, value);
}
private FPackageFileVersion _version;
public FPackageFileVersion Version
{
get => _version;
private set => SetProperty(ref _version, value);
}
public string Archive { get; }
public string MountPoint { get; }
public FPackageFileVersion Version { get; }
private string _searchText = string.Empty;
public string SearchText
@ -157,12 +134,12 @@ public class TreeItem : ViewModel
}
PathAtThisPoint = pathHere;
AssetsList.AssetsView.Filter = o => ItemFilter(o, SearchText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries));
AssetsList.SetFilter(o => ItemFilter(o, SearchText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries)));
}
private void RefreshFilters()
{
AssetsList.AssetsView.Refresh();
AssetsList.RefreshView();
FilteredFoldersView?.Refresh();
}
@ -195,11 +172,13 @@ public class TreeItem : ViewModel
RefreshFilters();
}
public override string ToString() => $"{Header} | {Folders.Count} Folders | {AssetsList.Assets.Count} Files";
public override string ToString() => $"{Header} | {Folders.Count} Folders | {AssetsList.Count} Files";
}
public class AssetsFolderViewModel
{
private Dictionary<string, TreeItem> _foldersByPath = new(StringComparer.Ordinal);
public RangeObservableCollection<TreeItem> Folders { get; }
public ICollectionView FoldersView { get; }
@ -209,85 +188,136 @@ public class AssetsFolderViewModel
FoldersView = new ListCollectionView(Folders) { SortDescriptions = { new SortDescription("Header", ListSortDirection.Ascending) } };
}
public void Clear()
{
Folders.Clear();
_foldersByPath = new Dictionary<string, TreeItem>(StringComparer.Ordinal);
}
public bool TryGetFolder(string directory, out TreeItem folder)
{
folder = null;
if (string.IsNullOrEmpty(directory))
return false;
directory = directory.TrimEnd(Path.AltDirectorySeparatorChar);
if (_foldersByPath.TryGetValue(directory, out folder))
return true;
// Preserve the previous behavior where only the root segment was case-insensitive.
var separator = directory.IndexOf(Path.AltDirectorySeparatorChar);
var rootName = separator < 0 ? directory : directory[..separator];
var root = Folders.FirstOrDefault(x => x.Header.Equals(rootName, StringComparison.OrdinalIgnoreCase));
if (root == null)
return false;
if (separator < 0)
{
folder = root;
return true;
}
return _foldersByPath.TryGetValue(string.Concat(root.Header, directory[separator..]), out folder);
}
public void BulkPopulate(IReadOnlyCollection<GameFile> entries)
{
if (entries == null || entries.Count == 0)
return;
var treeItems = new List<TreeItem>();
var foldersByPath = new Dictionary<string, TreeItem>(StringComparer.Ordinal);
var folderLookup = foldersByPath.GetAlternateLookup<ReadOnlySpan<char>>();
TreeItem previousFolder = null;
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
foreach (var entry in entries)
{
var path = entry.Path.AsSpan();
if (path.StartsWith(localAppData.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
path = path[localAppData.Length..];
while (!path.IsEmpty &&
(path[0] == Path.DirectorySeparatorChar || path[0] == Path.AltDirectorySeparatorChar))
path = path[1..];
}
var pathEnd = path.Length;
while (pathEnd > 0 && path[pathEnd - 1] == Path.AltDirectorySeparatorChar)
pathEnd--;
var lastSeparator = path[..pathEnd].LastIndexOf(Path.AltDirectorySeparatorChar);
if (lastSeparator < 0)
{
previousFolder = GetOrAddContentFolder(foldersByPath, entry, treeItems);
previousFolder.AssetsList.Add(entry);
continue;
}
var directories = path[..lastSeparator];
if (previousFolder != null && directories.SequenceEqual(previousFolder.PathAtThisPoint.AsSpan()))
{
previousFolder.AssetsList.Add(entry);
continue;
}
if (folderLookup.TryGetValue(directories, out var leafFolder))
{
leafFolder.AssetsList.Add(entry);
previousFolder = leafFolder;
continue;
}
TreeItem parentNode = null;
var segmentStart = 0;
while (segmentStart < directories.Length)
{
while (segmentStart < directories.Length && directories[segmentStart] == Path.AltDirectorySeparatorChar)
segmentStart++;
if (segmentStart == directories.Length)
break;
var segmentEnd = directories[segmentStart..].IndexOf(Path.AltDirectorySeparatorChar);
if (segmentEnd < 0)
segmentEnd = directories.Length;
else
segmentEnd += segmentStart;
var folderPath = directories[..segmentEnd];
if (!folderLookup.TryGetValue(folderPath, out var node))
{
var header = directories[segmentStart..segmentEnd].ToString();
var normalizedPath = parentNode == null
? header
: string.Concat(parentNode.PathAtThisPoint, "/", header);
if (!foldersByPath.TryGetValue(normalizedPath, out node))
{
node = new TreeItem(header, entry, normalizedPath) { Parent = parentNode };
foldersByPath.Add(normalizedPath, node);
if (parentNode == null)
treeItems.Add(node);
else
parentNode.Folders.AddWithoutNotification(node);
}
}
parentNode = node;
segmentStart = segmentEnd + 1;
}
if (parentNode == null)
parentNode = GetOrAddContentFolder(foldersByPath, entry, treeItems);
parentNode.AssetsList.Add(entry);
previousFolder = parentNode;
}
Application.Current.Dispatcher.Invoke(() =>
{
var treeItems = new RangeObservableCollection<TreeItem>();
treeItems.SetSuppressionState(true);
static TreeItem FindByHeaderOrNull(IReadOnlyList<TreeItem> list, string header)
{
for (var i = 0; i < list.Count; i++)
{
if (list[i].Header == header)
return list[i];
}
return null;
}
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
foreach (var entry in entries)
{
TreeItem lastNode = null;
TreeItem parentItem = null;
var path = entry.Path;
if (path.StartsWith(localAppData, StringComparison.OrdinalIgnoreCase))
path = path[localAppData.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var folders = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
var builder = new StringBuilder(64);
var parentNode = treeItems;
if (folders.Length <= 1)
{
var rootNode = FindByHeaderOrNull(treeItems, "Content");
if (rootNode == null)
{
rootNode = new TreeItem("Content", entry, "Content")
{
Parent = null
};
rootNode.Folders.SetSuppressionState(true);
rootNode.AssetsList.Assets.SetSuppressionState(true);
treeItems.Add(rootNode);
}
rootNode.AssetsList.Add(entry);
continue;
}
for (var i = 0; i < folders.Length - 1; i++)
{
var folder = folders[i];
builder.Append(folder).Append('/');
lastNode = FindByHeaderOrNull(parentNode, folder);
if (lastNode == null)
{
var nodePath = builder.ToString();
lastNode = new TreeItem(folder, entry, nodePath[..^1])
{
Parent = parentItem
};
lastNode.Folders.SetSuppressionState(true);
lastNode.AssetsList.Assets.SetSuppressionState(true);
parentNode.Add(lastNode);
}
parentItem = lastNode;
parentNode = lastNode.Folders;
}
lastNode?.AssetsList.Add(entry);
}
_foldersByPath = foldersByPath;
Folders.AddRange(treeItems);
if (treeItems.Count > 0)
@ -299,26 +329,19 @@ public class AssetsFolderViewModel
}
ApplicationService.ApplicationView.CUE4Parse.SearchVm.ChangeCollection(entries);
foreach (var folder in Folders)
InvokeOnCollectionChanged(folder);
static void InvokeOnCollectionChanged(TreeItem item)
{
item.Folders.SetSuppressionState(false);
item.AssetsList.Assets.SetSuppressionState(false);
if (item.Folders.Count != 0)
{
item.Folders.InvokeOnCollectionChanged();
foreach (var folderItem in item.Folders)
InvokeOnCollectionChanged(folderItem);
}
if (item.AssetsList.Assets.Count != 0)
item.AssetsList.Assets.InvokeOnCollectionChanged();
}
});
}
private static TreeItem GetOrAddContentFolder(Dictionary<string, TreeItem> foldersByPath, GameFile entry,
List<TreeItem> roots)
{
const string content = "Content";
if (foldersByPath.TryGetValue(content, out var node))
return node;
node = new TreeItem(content, entry, content);
foldersByPath.Add(content, node);
roots.Add(node);
return node;
}
}

View File

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Data;
using CUE4Parse.FileProvider.Objects;
@ -7,20 +9,58 @@ namespace FModel.ViewModels;
public class AssetsListViewModel
{
public RangeObservableCollection<GameFileViewModel> Assets { get; } = [];
private List<GameFile> _pendingAssets;
private RangeObservableCollection<GameFileViewModel> _assets;
public RangeObservableCollection<GameFileViewModel> Assets
{
get
{
if (_assets != null)
return _assets;
_assets = [];
if (_pendingAssets == null)
return _assets;
foreach (var asset in _pendingAssets)
_assets.AddWithoutNotification(new GameFileViewModel(asset));
_pendingAssets = null;
return _assets;
}
}
public int Count => _assets?.Count ?? _pendingAssets?.Count ?? 0;
private ICollectionView _assetsView;
private Predicate<object> _filter;
public ICollectionView AssetsView
{
get
{
_assetsView ??= new ListCollectionView(Assets)
{
SortDescriptions = { new SortDescription("Asset.Path", ListSortDirection.Ascending) }
SortDescriptions = { new SortDescription("Asset.Path", ListSortDirection.Ascending) },
Filter = _filter
};
return _assetsView;
}
}
public void Add(GameFile gameFile) => Assets.Add(new GameFileViewModel(gameFile));
public void Add(GameFile gameFile)
{
if (_assets == null)
(_pendingAssets ??= []).Add(gameFile);
else
_assets.Add(new GameFileViewModel(gameFile));
}
public void SetFilter(Predicate<object> filter)
{
_filter = filter;
if (_assetsView != null)
_assetsView.Filter = filter;
}
public void RefreshView() => _assetsView?.Refresh();
}

View File

@ -334,6 +334,7 @@ public class CUE4ParseViewModel : ViewModel
Provider.Initialize();
GameDirectory.AddLooseFiles(Provider.LooseFileCount);
GameDirectory.FlushPendingChanges();
_wwiseProviderLazy = new Lazy<WwiseProvider>(() => new WwiseProvider(Provider, UserSettings.Default.GameDirectory));
_fmodProviderLazy = new Lazy<FModProvider>(() => new FModProvider(Provider, UserSettings.Default.GameDirectory));
_criWareProviderLazy = new Lazy<CriWareProvider>(() => new CriWareProvider(Provider, UserSettings.Default.GameDirectory));
@ -379,6 +380,7 @@ public class CUE4ParseViewModel : ViewModel
{
Provider.SubmitKeys(aesKeys);
Provider.PostMount();
GameDirectory.FlushPendingChanges();
var aesMax = Provider.RequiredKeys.Count + Provider.Keys.Count;
var archiveMax = Provider.UnloadedVfs.Count + Provider.MountedVfs.Count;
@ -389,8 +391,8 @@ public class CUE4ParseViewModel : ViewModel
{
if (Provider == null) return;
AssetsFolder.Folders.Clear();
SearchVm.SearchResults.Clear();
AssetsFolder.Clear();
SearchVm.Clear();
Helper.CloseWindow<AdonisWindow>("Search For Packages");
Provider.UnloadNonStreamedVfs();
GC.Collect();

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FModel.Framework;
using FModel.Services;
@ -12,46 +13,41 @@ public class GoToCommand : ViewModelCommand<CustomDirectoriesViewModel>
{
}
public override void Execute(CustomDirectoriesViewModel contextViewModel, object parameter)
public override async void Execute(CustomDirectoriesViewModel contextViewModel, object parameter)
{
if (parameter is not string s || string.IsNullOrEmpty(s)) return;
JumpTo(s);
await JumpToAsync(s);
}
public TreeItem JumpTo(string directory)
public async Task<TreeItem> JumpToAsync(string directory)
{
_applicationView.SelectedLeftTabIndex = 1; // folders tab
var root = _applicationView.CUE4Parse.AssetsFolder.Folders;
if (root is not { Count: > 0 }) return null;
if (!_applicationView.CUE4Parse.AssetsFolder.TryGetFolder(directory, out var folder))
return null;
var i = 0;
var done = false;
var folders = directory.Split('/');
while (!done)
// An ancestor of the selected folder is already realized. Selecting it directly
// avoids running the virtualized path walker again (notably for breadcrumbs).
if (MainWindow.YesWeCats.AssetsFolderName.SelectedItem is TreeItem selectedFolder)
{
foreach (var folder in root)
for (var ancestor = selectedFolder; ancestor != null; ancestor = ancestor.Parent)
{
if (!folder.Header.Equals(folders[i], i == 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
if (!ReferenceEquals(ancestor, folder))
continue;
folder.IsExpanded = true; // folder found = expand
// is this the last folder aka the one we want to jump in
if (i >= folders.Length - 1)
{
folder.IsSelected = true; // select it
return folder;
}
root = folder.Folders; // grab his subfolders
break;
folder.IsSelected = true;
return folder;
}
i++;
done = i == folders.Length || root.Count == 0;
}
return null;
var ancestors = new Stack<TreeItem>();
for (var ancestor = folder; ancestor != null; ancestor = ancestor.Parent)
ancestors.Push(ancestor);
var path = new List<TreeItem>(ancestors.Count);
while (ancestors.TryPop(out var ancestor))
path.Add(ancestor);
return await MainWindow.YesWeCats.SelectFolderAsync(path) ? folder : null;
}
}

View File

@ -53,8 +53,8 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
#if DEBUG
var loadingTime = Stopwatch.StartNew();
#endif
_applicationView.CUE4Parse.AssetsFolder.Folders.Clear();
_applicationView.CUE4Parse.SearchVm.SearchResults.Clear();
_applicationView.CUE4Parse.AssetsFolder.Clear();
_applicationView.CUE4Parse.SearchVm.Clear();
_applicationView.SelectedLeftTabIndex = 1; // folders tab
_applicationView.IsAssetsExplorerVisible = true;
Helper.CloseWindow<AdonisWindow>("Search For Packages"); // close search window if opened

View File

@ -1,10 +1,12 @@
using FModel.Framework;
using System.Collections.ObjectModel;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using System.Windows.Data;
using System.Windows.Threading;
using CUE4Parse.Compression;
using CUE4Parse.UE4.IO;
using CUE4Parse.UE4.Objects.Core.Misc;
@ -123,11 +125,21 @@ public class FileItem : ViewModel
public partial class GameDirectoryViewModel : ViewModel
{
public readonly ObservableCollection<FileItem> DirectoryFiles;
private readonly record struct FileItemUpdate(FileItem File, bool IsEnabled, string MountPoint,
int FileCount, bool HasMountInfo);
public readonly RangeObservableCollection<FileItem> DirectoryFiles;
public ICollectionView DirectoryFilesView { get; }
private readonly Regex _hiddenArchives = ArchivesRegex();
private readonly ConcurrentDictionary<IAesVfsReader, FileItem> _filesByReader =
new(ReferenceEqualityComparer.Instance);
private readonly ConcurrentQueue<FileItem> _pendingAdditions = new();
private readonly ConcurrentQueue<FileItemUpdate> _pendingUpdates = new();
private FileItem _looseFilesContainer;
private int _pendingLooseFileCount;
private int _publishScheduled;
public GameDirectoryViewModel()
{
@ -147,7 +159,11 @@ public partial class GameDirectoryViewModel : ViewModel
if (!_hiddenArchives.IsMatch(reader.Name)) return;
var fileItem = new FileItem(reader);
Application.Current.Dispatcher.Invoke(() => DirectoryFiles.Add(fileItem));
if (!_filesByReader.TryAdd(reader, fileItem))
return;
_pendingAdditions.Enqueue(fileItem);
SchedulePublish();
}
public void AddLooseFiles(int fileCount)
@ -155,33 +171,78 @@ public partial class GameDirectoryViewModel : ViewModel
if (fileCount < 1)
return;
Application.Current.Dispatcher.Invoke(() =>
{
var looseFilesContainer = DirectoryFiles.FirstOrDefault(x => x.IsLooseFilesContainer);
if (looseFilesContainer is not null)
{
looseFilesContainer.FileCount += fileCount;
}
else
{
DirectoryFiles.Add(new FileItem("Loose Files", fileCount, 0, true));
}
});
Interlocked.Add(ref _pendingLooseFileCount, fileCount);
SchedulePublish();
}
public void Verify(IAesVfsReader reader)
{
if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return;
if (!_filesByReader.TryGetValue(reader, out var file)) return;
file.IsEnabled = true;
file.MountPoint = reader.MountPoint;
file.FileCount = reader.FileCount;
_pendingUpdates.Enqueue(new FileItemUpdate(file, true, reader.MountPoint, reader.FileCount, true));
SchedulePublish();
}
public void Disable(IAesVfsReader reader)
{
if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return;
file.IsEnabled = false;
if (!_filesByReader.TryGetValue(reader, out var file)) return;
_pendingUpdates.Enqueue(new FileItemUpdate(file, false, string.Empty, 0, false));
SchedulePublish();
}
public void FlushPendingChanges()
{
if (Application.Current.Dispatcher.CheckAccess())
PublishPendingChanges();
else
Application.Current.Dispatcher.Invoke(PublishPendingChanges);
}
private void SchedulePublish()
{
if (Interlocked.CompareExchange(ref _publishScheduled, 1, 0) != 0)
return;
_ = Application.Current.Dispatcher.BeginInvoke(PublishPendingChanges, DispatcherPriority.Background);
}
private void PublishPendingChanges()
{
var additions = new List<FileItem>();
while (_pendingAdditions.TryDequeue(out var file))
additions.Add(file);
var looseFileCount = Interlocked.Exchange(ref _pendingLooseFileCount, 0);
if (looseFileCount > 0)
{
if (_looseFilesContainer is null)
{
_looseFilesContainer = new FileItem("Loose Files", looseFileCount, 0, true);
additions.Add(_looseFilesContainer);
}
else
{
_looseFilesContainer.FileCount += looseFileCount;
}
}
if (additions.Count > 0)
DirectoryFiles.AddRange(additions);
while (_pendingUpdates.TryDequeue(out var update))
{
update.File.IsEnabled = update.IsEnabled;
if (!update.HasMountInfo)
continue;
update.File.MountPoint = update.MountPoint;
update.File.FileCount = update.FileCount;
}
Interlocked.Exchange(ref _publishScheduled, 0);
if (!_pendingAdditions.IsEmpty || !_pendingUpdates.IsEmpty || Volatile.Read(ref _pendingLooseFileCount) > 0)
SchedulePublish();
}
[GeneratedRegex(@"^(?!global|pakchunk.+(optional|ondemand)\-).+(pak|utoc)$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)]

View File

@ -67,6 +67,7 @@ namespace FModel.ViewModels;
public class GameFileViewModel(GameFile asset) : ViewModel
{
private const int MaxPreviewSize = 128;
private static readonly SemaphoreSlim ResolverConcurrency = new(4, 4);
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
private EGame? GameVersion => _applicationView.CUE4Parse?.Provider.Versions.Game;
@ -133,24 +134,24 @@ public class GameFileViewModel(GameFile asset) : ViewModel
=> ApplicationService.ThreadWorkerView.Begin(cancellationToken =>
_applicationView.CUE4Parse.ExtractSelected(cancellationToken, [Asset]));
public Task ResolveAsync(EResolveCompute resolve)
public async Task ResolveAsync(EResolveCompute resolve)
{
try
{
return ResolveInternalAsync(resolve);
await ResolveInternalAsync(resolve).ConfigureAwait(false);
}
catch (Exception e)
{
Log.Error(e, "Failed to resolve asset {AssetName} ({Resolver})", Asset.Path, resolve.ToStringBitfield());
Resolved = EResolveCompute.All;
return Task.CompletedTask;
}
}
private Task ResolveInternalAsync(EResolveCompute resolve)
{
if (!_applicationView.IsAssetsExplorerVisible || !UserSettings.Default.PreviewTexturesAssetExplorer)
if (!_applicationView.IsAssetsExplorerVisible || _applicationView.IsAssetPreviewLoadingSuspended ||
!UserSettings.Default.PreviewTexturesAssetExplorer)
{
resolve &= ~EResolveCompute.Preview;
}
@ -184,7 +185,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
return Task.CompletedTask;
}
return Task.Run(() =>
return RunResolverAsync(() =>
{
// TODO: cache and reuse packages
var pkg = _applicationView.CUE4Parse?.Provider.LoadPackage(Asset);
@ -405,7 +406,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
if (!resolve.HasFlag(EResolveCompute.Preview))
break;
return Task.Run(() =>
return RunResolverAsync(() =>
{
var data = _applicationView.CUE4Parse.Provider.SaveAsset(Asset);
using var stream = new MemoryStream(data);
@ -464,6 +465,19 @@ public class GameFileViewModel(GameFile asset) : ViewModel
return Task.CompletedTask;
}
private static async Task RunResolverAsync(Action action)
{
await ResolverConcurrency.WaitAsync().ConfigureAwait(false);
try
{
await Task.Run(action).ConfigureAwait(false);
}
finally
{
ResolverConcurrency.Release();
}
}
private void SetPreviewImage(SKData data)
{
using var ms = new MemoryStream(data.ToArray());
@ -492,7 +506,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
Task.Delay(100, token).ContinueWith(t =>
{
if (t.IsCanceled) return;
ResolveAsync(EResolveCompute.All);
_ = ResolveAsync(EResolveCompute.All);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}

View File

@ -61,33 +61,58 @@ public class SearchViewModel : ViewModel
private set => SetProperty(ref _refFile, value);
}
public RangeObservableCollection<GameFile> SearchResults { get; }
public ListCollectionView SearchResultsView { get; }
private List<GameFile> _searchResults = [];
public List<GameFile> SearchResults
{
get => _searchResults;
private set => SetProperty(ref _searchResults, value);
}
private ListCollectionView _searchResultsView;
private string[] _filters = [];
private Regex _filterRegex;
private bool _isRegexValid = true;
public ListCollectionView SearchResultsView
{
get
{
if (_searchResultsView != null)
return _searchResultsView;
PrepareFilter();
_searchResultsView = new ListCollectionView(SearchResults)
{
Filter = ItemFilter,
};
ResultsCount = _searchResultsView.Count;
return _searchResultsView;
}
}
public SearchViewModel()
{
SearchResults = [];
SearchResultsView = new ListCollectionView(SearchResults)
{
Filter = e => ItemFilter(e, FilterText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries)),
};
ResultsCount = SearchResultsView.Count;
ResultsCount = 0;
}
public void RefreshFilter()
{
PrepareFilter();
SearchResultsView.Refresh();
ResultsCount = SearchResultsView.Count;
}
public void ChangeCollection(IEnumerable<GameFile> files, GameFile refFile = null)
{
SearchResults.Clear();
SearchResults.AddRange(files);
var results = files as List<GameFile> ?? files.ToList();
_searchResultsView = null;
SearchResults = results;
RaisePropertyChanged(nameof(SearchResultsView));
RefFile = refFile;
ResultsCount = SearchResultsView.Count;
ResultsCount = results.Count;
}
public void Clear() => ChangeCollection([]);
public async Task CycleSortSizeMode()
{
CurrentSortSizeMode = CurrentSortSizeMode switch
@ -126,20 +151,41 @@ public class SearchViewModel : ViewModel
};
});
SearchResults.Clear();
SearchResults.AddRange(sorted);
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

@ -89,13 +89,13 @@ public partial class Breadcrumb
}
}
private void OnMouseClick(object sender, MouseButtonEventArgs e)
private async void OnMouseClick(object sender, MouseButtonEventArgs e)
{
if (sender is not Border { DataContext: string pathAtThisPoint, Tag: int index }) return;
var directory = string.Join('/', pathAtThisPoint.Split('/').Take(index));
if (pathAtThisPoint.Equals(directory)) return;
ApplicationService.ApplicationView.CustomDirectories.GoToCommand.JumpTo(directory);
await ApplicationService.ApplicationView.CustomDirectories.GoToCommand.JumpToAsync(directory);
}
}

View File

@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace FModel.Views.Resources.Controls;
/// <summary>
/// Exposes container realization for programmatic navigation in a virtualized tree.
/// </summary>
public sealed class NavigableVirtualizingStackPanel : VirtualizingStackPanel
{
public void BringItemIntoView(int index) => BringIndexIntoView(index);
}

View File

@ -100,7 +100,7 @@
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Opacity="0.8" />
<TextBlock Grid.Row="3"
Text="{Binding AssetsList.Assets.Count, StringFormat=Assets: {0}}"
Text="{Binding AssetsList.Count, StringFormat=Assets: {0}}"
FontSize="11"
HorizontalAlignment="Center"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"

View File

@ -76,7 +76,7 @@
<ColumnDefinition.Width>
<MultiBinding Converter="{x:Static converters:RatioToGridLengthConverter.Instance}">
<Binding Path="Folders.Count" />
<Binding Path="AssetsList.Assets.Count" />
<Binding Path="AssetsList.Count" />
</MultiBinding>
</ColumnDefinition.Width>
</ColumnDefinition>

View File

@ -39,7 +39,7 @@ public partial class ResourcesDictionary
// Auto expand single child folders
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];

View File

@ -105,6 +105,12 @@
Value="{Binding CUE4Parse.GameDirectory.DirectoryFilesView, IsAsync=True}" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Disabled" />
<Setter Property="ScrollViewer.CanContentScroll"
Value="True" />
<Setter Property="VirtualizingPanel.IsVirtualizing"
Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode"
Value="Recycling" />
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarExpansionMode"
Value="NeverExpand" />
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarPlacement"
@ -291,6 +297,19 @@
Value="Docked" />
<Setter Property="HorizontalContentAlignment"
Value="Stretch" />
<Setter Property="VirtualizingPanel.IsVirtualizing"
Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode"
Value="Recycling" />
<Setter Property="ScrollViewer.CanContentScroll"
Value="True" />
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<controls:NavigableVirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<HierarchicalDataTemplate ItemsSource="{Binding FoldersView, IsAsync=True}">
@ -308,7 +327,7 @@
Background="#705542"
Margin="-10.5 0 4.5 0"
VerticalAlignment="Bottom">
<TextBlock Text="{Binding AssetsList.Assets.Count}"
<TextBlock Text="{Binding AssetsList.Count}"
Margin="2.5 0 2.5 0"
Padding="1 0 0 0"
FontSize="7"
@ -325,7 +344,7 @@
<MultiBinding StringFormat="{}{0} has {1} folders and {2} packages">
<Binding Path="Header" />
<Binding Path="FoldersView.Count" />
<Binding Path="AssetsList.Assets.Count" />
<Binding Path="AssetsList.Count" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
@ -340,7 +359,7 @@
Property="Source"
Value="/FModel;component/Resources/empty_folder.png" />
</DataTrigger>
<DataTrigger Binding="{Binding AssetsList.Assets.Count}"
<DataTrigger Binding="{Binding AssetsList.Count}"
Value="0">
<Setter TargetName="TreeBadge"
Property="Visibility"
@ -572,6 +591,13 @@
BasedOn="{StaticResource TreeViewItemStyle}">
<Setter Property="HorizontalContentAlignment"
Value="Stretch" />
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<controls:NavigableVirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="IsExpanded"

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,
@ -141,30 +170,35 @@ public partial class SearchView
private async Task NavigateToAssetAndSelect(GameFile entry)
{
WindowState = WindowState.Minimized;
MainWindow.YesWeCats.AssetsListName.ItemsSource = null;
var folder = _applicationView.CustomDirectories.GoToCommand.JumpTo(entry.Directory);
if (folder == null)
return;
MainWindow.YesWeCats.Activate();
do
{ await Task.Delay(100); } while (MainWindow.YesWeCats.AssetsListName.Items.Count < folder.AssetsList.Assets.Count);
while (!folder.IsSelected || MainWindow.YesWeCats.AssetsFolderName.SelectedItem != folder)
await Task.Delay(50); // stops assets tab from opening too early
ApplicationService.ApplicationView.SelectedLeftTabIndex = 2; // assets tab
do
_applicationView.IsAssetPreviewLoadingSuspended = true;
try
{
await Task.Delay(100);
var vm = MainWindow.YesWeCats.AssetsListName.Items
.OfType<GameFileViewModel>()
.FirstOrDefault(x => x.Asset == entry);
WindowState = WindowState.Minimized;
MainWindow.YesWeCats.AssetsListName.ClearValue(ItemsControl.ItemsSourceProperty);
var folder = await _applicationView.CustomDirectories.GoToCommand.JumpToAsync(entry.Directory);
if (folder == null)
return;
MainWindow.YesWeCats.Activate();
while (MainWindow.YesWeCats.AssetsListName.Items.Count < folder.AssetsList.Count)
await Task.Delay(10);
ApplicationService.ApplicationView.SelectedLeftTabIndex = 2; // assets tab
GameFileViewModel vm;
while ((vm = MainWindow.YesWeCats.AssetsListName.Items
.OfType<GameFileViewModel>()
.FirstOrDefault(x => x.Asset == entry)) == null)
await Task.Delay(10);
MainWindow.YesWeCats.AssetsListName.SelectedItem = vm;
MainWindow.YesWeCats.AssetsListName.ScrollIntoView(vm);
} while (MainWindow.YesWeCats.AssetsListName.SelectedItem == null);
}
finally
{
_applicationView.IsAssetPreviewLoadingSuspended = false;
MainWindow.YesWeCats.RefreshVisibleAssetPreviews();
}
}
private async void OnAssetExtract(object sender, RoutedEventArgs e)
@ -182,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)