mirror of
https://github.com/4sval/FModel.git
synced 2026-08-07 11:36:03 -05:00
Merge b51a3cdd47 into 3d07a51d6d
This commit is contained in:
commit
f9acab6c4f
|
|
@ -1 +1 @@
|
|||
Subproject commit 396ede2a1d89752142c3b73dba7c908467ccf3a3
|
||||
Subproject commit 25d982ecf40620038525ebd7adbd07ed644df2fe
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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}}"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user