mirror of
https://github.com/4sval/FModel.git
synced 2026-08-06 03:03:04 -05:00
Optimize asset folder navigation
This commit is contained in:
parent
287435d0b3
commit
0e92b201f5
|
|
@ -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;
|
||||
|
|
@ -254,6 +257,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 +283,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;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ public class ApplicationViewModel : ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
public bool IsAssetPreviewLoadingSuspended { get; set; }
|
||||
|
||||
private int _selectedLeftTabIndex;
|
||||
public int SelectedLeftTabIndex
|
||||
{
|
||||
|
|
|
|||
|
|
@ -177,6 +177,8 @@ public sealed class TreeItem : ViewModel
|
|||
|
||||
public class AssetsFolderViewModel
|
||||
{
|
||||
private Dictionary<string, TreeItem> _foldersByPath = new(StringComparer.Ordinal);
|
||||
|
||||
public RangeObservableCollection<TreeItem> Folders { get; }
|
||||
public ICollectionView FoldersView { get; }
|
||||
|
||||
|
|
@ -186,6 +188,38 @@ 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)
|
||||
|
|
@ -283,6 +317,7 @@ public class AssetsFolderViewModel
|
|||
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
_foldersByPath = foldersByPath;
|
||||
Folders.AddRange(treeItems);
|
||||
|
||||
if (treeItems.Count > 0)
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@ public class CUE4ParseViewModel : ViewModel
|
|||
{
|
||||
if (Provider == null) return;
|
||||
|
||||
AssetsFolder.Folders.Clear();
|
||||
AssetsFolder.Clear();
|
||||
SearchVm.Clear();
|
||||
Helper.CloseWindow<AdonisWindow>("Search For Packages");
|
||||
Provider.UnloadNonStreamedVfs();
|
||||
|
|
|
|||
|
|
@ -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,7 +53,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
|
|||
#if DEBUG
|
||||
var loadingTime = Stopwatch.StartNew();
|
||||
#endif
|
||||
_applicationView.CUE4Parse.AssetsFolder.Folders.Clear();
|
||||
_applicationView.CUE4Parse.AssetsFolder.Clear();
|
||||
_applicationView.CUE4Parse.SearchVm.Clear();
|
||||
_applicationView.SelectedLeftTabIndex = 1; // folders tab
|
||||
_applicationView.IsAssetsExplorerVisible = true;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -291,6 +291,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}">
|
||||
|
|
@ -572,6 +585,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"
|
||||
|
|
|
|||
|
|
@ -141,30 +141,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.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.ItemsSource = null;
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user