Merge remote-tracking branch 'origin/dev' into feature/new-viewer

# Conflicts:
#	CUE4Parse
This commit is contained in:
Asval
2026-09-22 15:58:13 +02:00
18 changed files with 837 additions and 742 deletions

View File

@@ -0,0 +1,36 @@
using System.Windows.Controls;
namespace FModel.Extensions;
public static class ListBoxExtensions
{
public static ListBoxItem RevealItem(this ListBox list, object item, bool alignToTop = false)
{
var index = list.Items.IndexOf(item);
if (index < 0)
return null;
list.ApplyTemplate();
list.UpdateLayout();
if (alignToTop && list.FindVisualChild<ScrollViewer>() is { } scroll)
{
scroll.ScrollToVerticalOffset(index);
list.UpdateLayout();
}
if (list.FindVisualChild<VirtualizingPanel>() is { } panel)
{
panel.BringIndexIntoViewPublic(index);
}
else
{
list.ScrollIntoView(item);
}
list.UpdateLayout();
var container = list.ItemContainerGenerator.ContainerFromItem(item) as ListBoxItem;
container?.BringIntoView();
list.UpdateLayout();
return container;
}
}

View File

@@ -17,4 +17,20 @@ public static class VisualTreeExtensions
}
return null;
}
public static T? FindVisualChild<T>(this DependencyObject parent) where T : DependencyObject
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T result)
return result;
if (FindVisualChild<T>(child) is { } descendant)
return descendant;
}
return null;
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace FModel.Framework;
@@ -17,8 +18,7 @@ public sealed class RangeObservableCollection<T> : ObservableCollection<T>
public void AddRange(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException(nameof(list));
ArgumentNullException.ThrowIfNull(list);
_suppressNotification = true;
@@ -29,6 +29,24 @@ public sealed class RangeObservableCollection<T> : ObservableCollection<T>
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void ReplaceRange(IEnumerable<T> collection)
{
ArgumentNullException.ThrowIfNull(collection);
var items = collection as IList<T> ?? [.. collection];
CheckReentrancy();
Items.Clear();
foreach (var item in items)
Items.Add(item);
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void SetSuppressionState(bool state)
{
_suppressNotification = state;
@@ -38,4 +56,4 @@ public sealed class RangeObservableCollection<T> : ObservableCollection<T>
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(changedAction));
}
}
}

View File

@@ -344,16 +344,16 @@
<TextBlock HorizontalAlignment="Left" VerticalAlignment="Center" TextTrimming="CharacterEllipsis">
<TextBlock.Text>
<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="CUE4Parse.AssetsFolder.SelectedFolder.Header" FallbackValue="None" />
<Binding Path="CUE4Parse.AssetsFolder.SelectedFolder.Folders.Count" FallbackValue="0" />
<Binding Path="CUE4Parse.AssetsFolder.SelectedFolder.AssetsList.Assets.Count" FallbackValue="0" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" ToolTip="Bring Selected Folder To View" Padding="4"
Command="{Binding MenuCommand}" CommandParameter="{Binding SelectedItem, ElementName=AssetsFolderName}">
Command="{Binding MenuCommand}" CommandParameter="{Binding CUE4Parse.AssetsFolder.SelectedFolder}">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource LocateMeIcon}" />
@@ -379,13 +379,13 @@
</StackPanel>
</Grid>
<Separator Grid.Row="1" Style="{StaticResource CustomSeparator}" Margin="0" />
<TreeView Grid.Row="2"
x:Name="AssetsFolderName"
Style="{StaticResource AssetsFolderTreeView}"
SelectedItemChanged="OnAssetsTreeSelectedItemChanged"
PreviewKeyDown="OnFoldersPreviewKeyDown"
PreviewMouseDoubleClick="OnAssetsTreeMouseDoubleClick">
</TreeView>
<controls:FolderTree Grid.Row="2" x:Name="AssetsFolderName"
Style="{StaticResource AssetsFolderListBox}"
Folders="{Binding CUE4Parse.AssetsFolder}"
ItemsSource="{Binding CUE4Parse.AssetsFolder.VisibleFolders}"
SelectedItem="{Binding CUE4Parse.AssetsFolder.SelectedFolder, Mode=TwoWay}"
SelectionChanged="OnAssetsTreeSelectedItemChanged"
OpenAssets="OnFolderOpenAssets" />
<Separator Grid.Row="3" Style="{StaticResource CustomSeparator}" Tag="INFORMATION" />
<StackPanel Grid.Row="4" Orientation="Vertical" Margin="0 0 0 5">
<Grid HorizontalAlignment="Stretch">
@@ -401,27 +401,27 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding SelectedItem.AssetsList.Assets.Count, ElementName=AssetsFolderName, FallbackValue=0, StringFormat={}{0:N0}}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding CUE4Parse.AssetsFolder.SelectedFolder.AssetsList.Assets.Count, FallbackValue=0, StringFormat={}{0:N0}}" 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, StringFormat={}{0:N0}}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding CUE4Parse.AssetsFolder.SelectedFolder.Folders.Count, FallbackValue=0, StringFormat={}{0:N0}}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="Folders Count" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding SelectedItem.Archive, ElementName=AssetsFolderName, FallbackValue='None'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding CUE4Parse.AssetsFolder.SelectedFolder.Archive, FallbackValue='None'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.Column="1" Text="Included In Archive" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="{Binding SelectedItem.MountPoint, ElementName=AssetsFolderName, FallbackValue='/', Converter={x:Static converters:TrimRightToLeftConverter.Instance}, ConverterParameter=275}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="{Binding CUE4Parse.AssetsFolder.SelectedFolder.MountPoint, FallbackValue='/', Converter={x:Static converters:TrimRightToLeftConverter.Instance}, ConverterParameter=275}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="3" Grid.Column="1" Text="Archive Mount Point" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="{Binding SelectedItem.Version, ElementName=AssetsFolderName, FallbackValue='VER_UE4_LATEST', Converter={x:Static converters:TrimRightToLeftConverter.Instance}, ConverterParameter=275}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="{Binding CUE4Parse.AssetsFolder.SelectedFolder.Version, FallbackValue='VER_UE4_LATEST', Converter={x:Static converters:TrimRightToLeftConverter.Instance}, ConverterParameter=275}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="4" Grid.Column="1" Text="Archive Version" VerticalAlignment="Center" HorizontalAlignment="Right" />
</Grid>
</StackPanel>
</Grid>
</TabItem>
<TabItem Style="{StaticResource TabItemFillSpace}"
Header="{Binding SelectedItem.AssetsList.Assets.Count, FallbackValue=0, ElementName=AssetsFolderName}"
Header="{Binding CUE4Parse.AssetsFolder.SelectedFolder.AssetsList.Assets.Count, FallbackValue=0}"
HeaderStringFormat="{}{0} Packages">
<DockPanel>
<inputs:SearchTextBox DockPanel.Dock="Top" x:Name="AssetsSearchTextBox"
Text="{Binding SelectedItem.SearchText, ElementName=AssetsFolderName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding CUE4Parse.AssetsFolder.SelectedFolder.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ClearButtonClick="OnClearFilterClick" />
<Grid DockPanel.Dock="Top">
@@ -434,7 +434,7 @@
<controls:Breadcrumb Grid.Row="0" HorizontalAlignment="Left" Margin="0 5 0 5"
MaxWidth="{Binding ActualWidth, ElementName=AssetsSearchTextBox}"
DataContext="{Binding SelectedItem.PathAtThisPoint, ElementName=AssetsFolderName, FallbackValue='No/Directory/Detected/In/Folder'}"/>
DataContext="{Binding CUE4Parse.AssetsFolder.SelectedFolder.PathAtThisPoint, FallbackValue='No/Directory/Detected/In/Folder'}"/>
<ListBox Grid.Row="1" x:Name="AssetsListName"
Style="{StaticResource AssetsListBox}"
@@ -508,12 +508,12 @@
</Grid.ColumnDefinitions>
<inputs:SearchTextBox x:Name="AssetsExplorerSearch" Grid.Column="0"
Text="{Binding SelectedItem.SearchText, ElementName=AssetsFolderName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding CUE4Parse.AssetsFolder.SelectedFolder.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ClearButtonClick="OnClearFilterClick" />
<ComboBox x:Name="CategoriesSelector" Grid.Column="2"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedItem.SelectedCategory, ElementName=AssetsFolderName, Mode=TwoWay}" />
SelectedItem="{Binding CUE4Parse.AssetsFolder.SelectedFolder.SelectedCategory, Mode=TwoWay}" />
<StackPanel Grid.Column="4"
Orientation="Horizontal"
@@ -555,11 +555,11 @@
<controls:Breadcrumb Grid.Row="1" Margin="0 5 0 0"
HorizontalAlignment="Left"
DataContext="{Binding SelectedItem.PathAtThisPoint, ElementName=AssetsFolderName}" />
DataContext="{Binding CUE4Parse.AssetsFolder.SelectedFolder.PathAtThisPoint}" />
<Grid Grid.Row="2">
<ListBox x:Name="AssetsExplorer"
ItemsSource="{Binding SelectedItem.CombinedEntries, ElementName=AssetsFolderName, IsAsync=True}"
ItemsSource="{Binding CUE4Parse.AssetsFolder.SelectedFolder.CombinedEntries}"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
PreviewKeyDown="OnPreviewKeyDown">
<ListBox.Style>
@@ -577,7 +577,7 @@
</ListBox>
<!-- once we need sorting, convert this to a listview (see search window) and add the header back (customize the template to re-add GridViewHeaderRowPresenter) -->
<ListBox x:Name="AssetsListExplorer"
ItemsSource="{Binding SelectedItem.CombinedEntries, ElementName=AssetsFolderName, IsAsync=True}"
ItemsSource="{Binding CUE4Parse.AssetsFolder.SelectedFolder.CombinedEntries}"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
PreviewKeyDown="OnPreviewKeyDown">
<ListBox.Style>

View File

@@ -23,7 +23,7 @@ namespace FModel;
/// </summary>
public partial class MainWindow
{
public static MainWindow YesWeCats;
public static MainWindow Instance => (MainWindow) Application.Current.MainWindow;
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
private DiscordHandler _discordHandler => DiscordService.DiscordHandler;
@@ -48,7 +48,7 @@ public partial class MainWindow
else if (LeftTabControl.SelectedIndex == 1 && AssetsFolderName.SelectedItem is TreeItem { Parent: TreeItem parent })
{
AssetsFolderName.Focus();
parent.IsSelected = true;
SelectFolder(parent);
}
}));
@@ -63,7 +63,6 @@ public partial class MainWindow
AssetsListName.SelectionChanged += (_, e) => SyncSelection(AssetsExplorer, e);
FLogger.Logger = LogRtbName;
YesWeCats = this;
}
// Hack to sync selection between packages tab and explorer
@@ -236,7 +235,7 @@ public partial class MainWindow
DirectoryFilesListBox.Focus();
break;
case 1:
AssetsFolderName.Focus();
AssetsFolderName.FocusSelection();
break;
case 2:
AssetsListName.Focus();
@@ -264,12 +263,7 @@ public partial class MainWindow
}
}
private void OnAssetsTreeMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is not TreeView { SelectedItem: TreeItem treeItem } || treeItem.Folders.Count > 0) return;
_applicationView.SelectedLeftTabIndex++;
}
private void OnFolderOpenAssets(object sender, EventArgs e) => _applicationView.SelectedLeftTabIndex = 2;
private void OnPreviewTexturesToggled(object sender, RoutedEventArgs e) => ItemContainerGenerator_StatusChanged(AssetsExplorer.ItemContainerGenerator, EventArgs.Empty);
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
@@ -297,14 +291,43 @@ public partial class MainWindow
}
}
private void OnAssetsTreeSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
private void OnAssetsTreeSelectedItemChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not TreeView { SelectedItem: TreeItem }) return;
if (sender is not ListBox { SelectedItem: TreeItem })
return;
_applicationView.IsAssetsExplorerVisible = true;
_applicationView.SelectedLeftTabIndex = 1;
}
public void SelectFolder(TreeItem folder)
{
_applicationView.SelectedLeftTabIndex = 1;
UpdateLayout();
AssetsFolderName.SelectFolder(folder);
AssetsFolderName.FocusSelection();
}
public void SelectAsset(GameFileViewModel asset)
{
var useExplorer = UserSettings.Default.FeaturePreviewNewAssetExplorer;
_applicationView.SelectedLeftTabIndex = useExplorer ? 1 : 2;
if (useExplorer)
{
_applicationView.IsAssetsExplorerVisible = true;
}
var list = useExplorer ? UserSettings.Default.ExplorerViewMode == EExplorerViewMode.List ? AssetsListExplorer : AssetsExplorer : AssetsListName;
list.GetBindingExpression(ItemsControl.ItemsSourceProperty)?.UpdateTarget();
list.UnselectAll();
list.SelectedItem = asset;
UpdateLayout();
var container = list.RevealItem(asset);
Activate();
container?.Focus();
}
private async void OnAssetsListMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is not ListBox listBox) return;
@@ -348,24 +371,18 @@ public partial class MainWindow
await _threadWorkerView.Begin(cancellationToken => _applicationView.CUE4Parse.ExtractSelected(cancellationToken, [file.Asset]));
break;
case TreeItem folder:
e.Handled = true;
ApplicationService.ApplicationView.SelectedLeftTabIndex = 1;
var parent = folder.Parent;
while (parent != null)
{
parent.IsExpanded = true;
parent = parent.Parent;
}
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
{
childFolder.IsExpanded = true;
_applicationView.CUE4Parse.AssetsFolder.Expand(childFolder);
childFolder = childFolder.Folders[0];
}
childFolder.IsExpanded = true;
childFolder.IsSelected = true;
_applicationView.CUE4Parse.AssetsFolder.Expand(childFolder);
SelectFolder(childFolder);
break;
}
}
@@ -375,34 +392,12 @@ public partial class MainWindow
_applicationView.IsAssetsExplorerVisible = false;
}
private async void OnFoldersPreviewKeyDown(object sender, KeyEventArgs e)
{
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)
{
_applicationView.SelectedLeftTabIndex++;
return;
}
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];
}
childFolder.IsExpanded = true;
childFolder.IsSelected = true;
}
private void OnExportHotkey(string trigger)
{
if (!_applicationView.Status.IsReady || Keyboard.FocusedElement is not DependencyObject focused)
return;
IList selection = focused.FindAncestor<TreeView>() == AssetsFolderName
IList selection = focused.FindAncestor<ListBox>() == AssetsFolderName
? new[] { AssetsFolderName.SelectedItem }
: focused.FindAncestor<ListBox>()?.SelectedItems;

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -80,7 +79,16 @@ public class TreeItem : ViewModel
set
{
if (SetProperty(ref _selectedCategory, value))
_ = OnSelectedCategoryChanged();
{
if (value == EAssetCategory.All)
{
RefreshFilters();
}
else
{
_ = OnSelectedCategoryChanged();
}
}
}
}
@@ -88,19 +96,6 @@ public class TreeItem : ViewModel
public AssetsListViewModel AssetsList { get; } = new();
public RangeObservableCollection<TreeItem> Folders { get; } = [];
private ICollectionView _foldersView;
public ICollectionView FoldersView
{
get
{
_foldersView ??= new ListCollectionView(Folders)
{
SortDescriptions = { new SortDescription(nameof(Header), ListSortDirection.Ascending) }
};
return _foldersView;
}
}
private ICollectionView? _filteredFoldersView;
public ICollectionView? FilteredFoldersView
{
@@ -116,35 +111,15 @@ public class TreeItem : ViewModel
}
private CompositeCollection _combinedEntries;
public CompositeCollection CombinedEntries
{
get
{
if (_combinedEntries == null)
{
void CreateCombinedEntries()
{
_combinedEntries = new CompositeCollection
{
new CollectionContainer { Collection = FilteredFoldersView },
new CollectionContainer { Collection = AssetsList.AssetsView }
};
}
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(CreateCombinedEntries);
}
else
{
CreateCombinedEntries();
}
}
return _combinedEntries;
}
}
public CompositeCollection CombinedEntries => _combinedEntries ??=
[
new CollectionContainer { Collection = FilteredFoldersView },
new CollectionContainer { Collection = AssetsList.AssetsView }
];
public TreeItem Parent { get; init; }
public int Depth => Parent?.Depth + 1 ?? 0;
public Thickness Indent => new(Depth * 16, 0, 0, 0); // For folder tree indentation
public TreeItem(string header, GameFile entry, string pathHere)
{
@@ -198,15 +173,177 @@ public class TreeItem : ViewModel
public override string ToString() => $"{Header} | {Folders.Count} Folders | {AssetsList.Assets.Count} Files";
}
public class AssetsFolderViewModel
public class AssetsFolderViewModel : ViewModel
{
public RangeObservableCollection<TreeItem> Folders { get; }
public ICollectionView FoldersView { get; }
public RangeObservableCollection<TreeItem> Folders { get; } = [];
public RangeObservableCollection<TreeItem> VisibleFolders { get; } = [];
public AssetsFolderViewModel()
private TreeItem? _selectedFolder;
public TreeItem? SelectedFolder
{
Folders = [];
FoldersView = new ListCollectionView(Folders) { SortDescriptions = { new SortDescription("Header", ListSortDirection.Ascending) } };
get => _selectedFolder;
set
{
var previous = _selectedFolder;
if (!SetProperty(ref _selectedFolder, value))
return;
previous?.IsSelected = false;
value?.IsSelected = true;
}
}
public void Clear()
{
SelectedFolder = null;
VisibleFolders.Clear();
Folders.Clear();
}
public void CollapseAll()
{
static void CollapseChildren(TreeItem folder)
{
folder.IsExpanded = false;
foreach (var child in folder.Folders)
{
CollapseChildren(child);
}
}
var selected = SelectedFolder;
while (selected?.Parent != null)
selected = selected.Parent;
foreach (var folder in Folders)
{
CollapseChildren(folder);
}
SelectedFolder = selected;
for (var i = VisibleFolders.Count - 1; i >= 0; i--)
{
if (VisibleFolders[i].Parent != null)
{
VisibleFolders.RemoveAt(i);
}
}
}
public void Expand(TreeItem folder)
{
if (folder.Folders.Count == 0)
return;
var index = VisibleFolders.IndexOf(folder);
if (index < 0)
return;
if (index + 1 < VisibleFolders.Count && ReferenceEquals(VisibleFolders[index + 1].Parent, folder))
{
folder.IsExpanded = true;
return;
}
foreach (var child in EnumerateVisibleChildren(folder))
{
VisibleFolders.Insert(++index, child);
}
folder.IsExpanded = true;
}
public void Collapse(TreeItem folder)
{
if (!folder.IsExpanded)
return;
var index = VisibleFolders.IndexOf(folder);
if (index < 0)
return;
var selected = SelectedFolder;
var start = index + 1;
var count = 0;
while (start + count < VisibleFolders.Count && VisibleFolders[start + count].Depth > folder.Depth)
count++;
if (selected != null && IsDescendantOf(selected, folder))
{
SelectedFolder = folder;
}
for (var i = start + count - 1; i >= start; i--)
{
VisibleFolders.RemoveAt(i);
}
folder.IsExpanded = false;
}
public void Toggle(TreeItem folder)
{
if (folder.IsExpanded)
{
Collapse(folder);
}
else
{
Expand(folder);
}
}
public void Reveal(TreeItem folder)
{
var parents = new Stack<TreeItem>();
for (var parent = folder.Parent; parent != null; parent = parent.Parent)
parents.Push(parent);
while (parents.Count > 0)
Expand(parents.Pop());
SelectedFolder = folder;
}
private static bool IsDescendantOf(TreeItem item, TreeItem parent)
{
for (var current = item.Parent; current != null; current = current.Parent)
{
if (ReferenceEquals(current, parent))
return true;
}
return false;
}
private static IEnumerable<TreeItem> EnumerateVisibleChildren(TreeItem folder)
{
foreach (var child in folder.Folders)
{
yield return child;
if (!child.IsExpanded)
continue;
foreach (var descendant in EnumerateVisibleChildren(child))
{
yield return descendant;
}
}
}
private static void SortFolders(RangeObservableCollection<TreeItem> folders)
{
if (folders.Count > 1)
{
folders.ReplaceRange([.. folders.OrderBy(x => x.Header, StringComparer.OrdinalIgnoreCase)]);
}
foreach (var folder in folders)
{
SortFolders(folder.Folders);
}
}
public void BulkPopulate(IReadOnlyCollection<GameFile> entries)
@@ -283,14 +420,15 @@ public class AssetsFolderViewModel
lastNode?.AssetsList.Add(entry);
}
SortFolders(treeItems);
Folders.AddRange(treeItems);
VisibleFolders.AddRange(treeItems);
if (treeItems.Count > 0)
{
// Select after publishing the collection. Selecting a detached TreeItem lets WPF
// auto-select the first root (usually the synthetic "Content" bucket) instead.
var projectName = ApplicationService.ApplicationView.CUE4Parse.Provider.ProjectName;
(treeItems.FirstOrDefault(x => x.Header.Equals(projectName, StringComparison.OrdinalIgnoreCase)) ?? treeItems[0]).IsSelected = true;
SelectedFolder = treeItems.FirstOrDefault(x => x.Header.Equals(projectName, StringComparison.OrdinalIgnoreCase)) ?? treeItems[0];
}
ApplicationService.ApplicationView.CUE4Parse.SearchVm.ChangeCollection(entries);

View File

@@ -347,7 +347,7 @@ public class CUE4ParseViewModel : ViewModel
{
if (Provider == null) return;
AssetsFolder.Folders.Clear();
AssetsFolder.Clear();
SearchVm.SearchResults.Clear();
Helper.CloseWindow<AdonisWindow>("Search For Packages");
Provider.UnloadNonStreamedVfs();

View File

@@ -4,54 +4,56 @@ using FModel.Services;
namespace FModel.ViewModels.Commands;
public class GoToCommand : ViewModelCommand<CustomDirectoriesViewModel>
public class GoToCommand(CustomDirectoriesViewModel contextViewModel) : ViewModelCommand<CustomDirectoriesViewModel>(contextViewModel)
{
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
public GoToCommand(CustomDirectoriesViewModel contextViewModel) : base(contextViewModel)
{
}
public override void Execute(CustomDirectoriesViewModel contextViewModel, object parameter)
{
if (parameter is not string s || string.IsNullOrEmpty(s)) return;
JumpTo(s);
var folder = JumpTo(s);
if (folder != null)
{
MainWindow.Instance.SelectFolder(folder);
}
}
public TreeItem JumpTo(string directory)
{
_applicationView.SelectedLeftTabIndex = 1; // folders tab
var root = _applicationView.CUE4Parse.AssetsFolder.Folders;
if (root is not { Count: > 0 }) return null;
_applicationView.SelectedLeftTabIndex = 1;
var i = 0;
var done = false;
var folders = directory.Split('/');
while (!done)
var current = _applicationView.CUE4Parse.AssetsFolder.Folders;
if (current.Count == 0)
return null;
var folders = directory.Split('/', StringSplitOptions.RemoveEmptyEntries);
TreeItem result = null;
for (var i = 0; i < folders.Length; i++)
{
foreach (var folder in root)
result = null;
foreach (var folder in current)
{
if (!folder.Header.Equals(folders[i], i == 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
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
result = folder;
break;
}
i++;
done = i == folders.Length || root.Count == 0;
if (result == null)
return null;
current = result.Folders;
}
return null;
if (result != null)
{
_applicationView.CUE4Parse.AssetsFolder.Reveal(result);
}
return result;
}
}

View File

@@ -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.SearchResults.Clear();
_applicationView.SelectedLeftTabIndex = 1; // folders tab
_applicationView.IsAssetsExplorerVisible = true;

View File

@@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using AdonisUI.Controls;
using FModel.Extensions;
using FModel.Framework;
@@ -73,60 +71,13 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
case "ToolBox_Open_Output_Directory":
Process.Start(new ProcessStartInfo { FileName = UserSettings.Default.OutputDirectory, UseShellExecute = true });
break;
// case "ToolBox_Expand_All":
// await ApplicationService.ThreadWorkerView.Begin(cancellationToken =>
// {
// SetFoldersIsExpanded(contextViewModel.CUE4Parse.AssetsFolder, true, cancellationToken);
// });
// break;
case "ToolBox_Collapse_All":
await ApplicationService.ThreadWorkerView.Begin(cancellationToken =>
{
SetFoldersIsExpanded(contextViewModel.CUE4Parse.AssetsFolder, false, cancellationToken);
});
contextViewModel.CUE4Parse.AssetsFolder.CollapseAll();
break;
case TreeItem selectedFolder:
selectedFolder.IsSelected = false;
selectedFolder.IsSelected = true;
MainWindow.Instance.SelectFolder(selectedFolder);
break;
}
}
private void SetFoldersIsExpanded(AssetsFolderViewModel root, bool expand, CancellationToken cancellationToken)
{
var nodes = new LinkedList<TreeItem>();
foreach (TreeItem folder in root.Folders)
nodes.AddLast(folder);
var current = nodes.First;
while (current != null)
{
var folder = current.Value;
// Collapse top-down (reduce layout updates)
if (!expand && folder.IsExpanded)
{
folder.IsExpanded = false;
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
}
foreach (var child in folder.Folders)
{
nodes.AddLast(child);
}
current = current.Next;
}
if (!expand) return;
// Expand bottom-up (reduce layout updates)
for (var node = nodes.Last; node != null; node = node.Previous)
{
node.Value.IsExpanded = true;
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
}
}
}

View File

@@ -96,6 +96,6 @@ public partial class Breadcrumb
var directory = string.Join('/', pathAtThisPoint.Split('/').Take(index));
if (pathAtThisPoint.Equals(directory)) return;
ApplicationService.ApplicationView.CustomDirectories.GoToCommand.JumpTo(directory);
ApplicationService.ApplicationView.CustomDirectories.GoToCommand.Execute(directory);
}
}

View File

@@ -243,7 +243,7 @@
TargetType="ListBox"
BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="ItemsSource"
Value="{Binding SelectedItem.AssetsList.AssetsView, ElementName=AssetsFolderName, IsAsync=True}" />
Value="{Binding CUE4Parse.AssetsFolder.SelectedFolder.AssetsList.AssetsView}" />
<Setter Property="SelectionMode"
Value="Extended" />
<Setter Property="ContextMenu"

View File

@@ -29,24 +29,12 @@ public partial class ResourcesDictionary
case TreeItem folder:
ApplicationService.ApplicationView.SelectedLeftTabIndex = 1;
// Expand all parent folders if not expanded
var parent = folder.Parent;
while (parent != null)
{
parent.IsExpanded = true;
parent = parent.Parent;
}
// Auto expand single child folders
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];
}
childFolder.IsExpanded = true;
childFolder.IsSelected = true;
MainWindow.Instance.SelectFolder(childFolder);
ApplicationService.ApplicationView.CUE4Parse.AssetsFolder.Expand(childFolder);
break;
}
}

View File

@@ -0,0 +1,158 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using FModel.Extensions;
using FModel.ViewModels;
namespace FModel.Views.Resources.Controls;
public sealed class FolderTree : ListBox
{
public static readonly DependencyProperty FoldersProperty = DependencyProperty.Register(nameof(Folders), typeof(AssetsFolderViewModel), typeof(FolderTree));
public AssetsFolderViewModel Folders
{
get => (AssetsFolderViewModel) GetValue(FoldersProperty);
set => SetValue(FoldersProperty, value);
}
public event EventHandler OpenAssets;
public FolderTree()
{
IsVisibleChanged += (_, e) =>
{
if (e.NewValue is true)
{
RevealSelection();
}
};
}
public void SelectFolder(TreeItem folder)
{
Folders.Reveal(folder);
SetCurrentValue(SelectedItemProperty, folder);
this.RevealItem(folder);
}
public void FocusSelection()
{
if (SelectedItem is TreeItem folder)
{
this.RevealItem(folder)?.Focus();
}
else
{
Focus();
}
}
public void RevealSelection()
{
if (SelectedItem is TreeItem folder)
{
this.RevealItem(folder, alignToTop: true);
}
}
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (e.OriginalSource is DependencyObject source && source.FindAncestor<ToggleButton>() is { DataContext: TreeItem folder } && ContainerFromElement(this, source) is ListBoxItem)
{
e.Handled = true;
Folders?.Toggle(folder);
return;
}
base.OnPreviewMouseLeftButtonDown(e);
}
protected override void OnPreviewMouseDoubleClick(MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left && e.OriginalSource is DependencyObject source && source.FindAncestor<ToggleButton>() == null && ContainerFromElement(this, source) is ListBoxItem { DataContext: TreeItem folder })
{
e.Handled = true;
if (folder.Folders.Count > 0)
{
Folders?.Toggle(folder);
}
else if (folder.AssetsList.Assets.Count > 0)
{
OpenAssets?.Invoke(this, EventArgs.Empty);
}
}
base.OnPreviewMouseDoubleClick(e);
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
if (Folders == null || SelectedItem is not TreeItem folder)
{
base.OnPreviewKeyDown(e);
return;
}
switch (e.Key)
{
case Key.Up:
SelectFolder((TreeItem) Items[Math.Max(0, SelectedIndex - 1)]);
break;
case Key.Down:
SelectFolder((TreeItem) Items[Math.Min(Items.Count - 1, SelectedIndex + 1)]);
break;
case Key.Home:
SelectFolder((TreeItem) Items[0]);
break;
case Key.End:
SelectFolder((TreeItem) Items[Items.Count - 1]);
break;
case Key.Left:
if (folder.IsExpanded)
{
Folders?.Toggle(folder);
}
else if (folder.Parent != null)
{
SelectFolder(folder.Parent);
}
break;
case Key.Right:
if (!folder.IsExpanded)
{
Folders?.Toggle(folder);
}
else if (folder.Folders.Count > 0)
{
SelectFolder(folder.Folders[0]);
}
break;
case Key.Enter:
if ((folder.IsExpanded || folder.Folders.Count == 0) && folder.AssetsList.Assets.Count > 0)
{
e.Handled = true;
OpenAssets?.Invoke(this, EventArgs.Empty);
return;
}
while (folder.Folders.Count == 1 && folder.AssetsList.Assets.Count == 0)
folder = folder.Folders[0];
SelectFolder(folder);
if (!folder.IsExpanded)
{
Folders?.Toggle(folder);
}
break;
default:
base.OnPreviewKeyDown(e);
return;
}
e.Handled = true;
FocusSelection();
}
}

View File

@@ -1,41 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace FModel.Views.Resources.Controls;
public sealed class TreeViewItemBehavior
{
public static bool GetIsBroughtIntoViewWhenSelected(TreeViewItem treeViewItem)
{
return (bool) treeViewItem.GetValue(IsBroughtIntoViewWhenSelectedProperty);
}
public static void SetIsBroughtIntoViewWhenSelected(TreeViewItem treeViewItem, bool value)
{
treeViewItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
}
public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty =
DependencyProperty.RegisterAttached("IsBroughtIntoViewWhenSelected", typeof(bool), typeof(TreeViewItemBehavior),
new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged));
private static void OnIsBroughtIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (depObj is not TreeViewItem item)
return;
if (e.NewValue is not bool value)
return;
if (value)
item.Selected += OnTreeViewItemSelected;
else
item.Selected -= OnTreeViewItemSelected;
}
private static void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is TreeViewItem item)
item.BringIntoView();
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace FModel.Views.Resources.Converters;
public sealed class FolderTreeIconConverter : IValueConverter
{
public static readonly FolderTreeIconConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var name = value as string;
// This should get reworked to be aligned with asset explorer somehow
return name?.ToLowerInvariant() switch
{
"config" => "/FModel;component/Resources/gear.png",
"plugins" => "/FModel;component/Resources/puzzle.png",
"windows" => "/FModel;component/Resources/windows.png",
"android" => "/FModel;component/Resources/android.png",
"ios" or "mac" => "/FModel;component/Resources/apple.png",
"linux" or "linuxaarch64" => "/FModel;component/Resources/linux.png",
"unix" => "/FModel;component/Resources/unix.png",
"platforms" => "/FModel;component/Resources/pc.png",
"weapons" => "/FModel;component/Resources/weapon.png",
"localization" or "internationalization" => "/FModel;component/Resources/localization.png",
"ui" => "/FModel;component/Resources/ui.png",
"sounds" or "audio" => "/FModel;component/Resources/sound.png",
"2dassets" or "textures" => "/FModel;component/Resources/texture.png",
"engine" or "enginecontent" => "/FModel;component/Resources/engine.png",
"materials" => "/FModel;component/Resources/materialicon.png",
"blueprints" => "/FModel;component/Resources/blueprint.png",
"cinematics" or "movies" => "/FModel;component/Resources/cinematics.png",
"fortnite" => "/FModel;component/Resources/fortnite.png",
"fortnitegame" => "/FModel;component/Resources/fortnitebr.png",
"worldexplorers" => "/FModel;component/Resources/battlebreakers.png",
"oakgame" => "/FModel;component/Resources/borderlands.png",
"shootergame" => "/FModel;component/Resources/valorant.png",
"roguecompany" => "/FModel;component/Resources/roguecompany.png",
"g3" => "/FModel;component/Resources/spellbreak.png",
"swgame" => "/FModel;component/Resources/fallenorder.png",
"prospect" => "/FModel;component/Resources/thecycle.png",
"creative" => "/FModel;component/Resources/creative.png",
"athena" => "/FModel;component/Resources/athena.png",
"stateofdecay2" => "/FModel;component/Resources/stateofdecay2.png",
_ => null
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

View File

@@ -230,69 +230,251 @@
</Style.Triggers>
</Style>
<Style x:Key="AssetsFolderTreeView"
TargetType="TreeView"
BasedOn="{StaticResource {x:Type TreeView}}">
<Setter Property="ItemsSource"
Value="{Binding CUE4Parse.AssetsFolder.FoldersView, IsAsync=True}" />
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarExpansionMode"
Value="NeverExpand" />
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarPlacement"
Value="Docked" />
<Setter Property="adonisExtensions:ScrollViewerExtension.HorizontalScrollBarExpansionMode"
Value="NeverExpand" />
<Setter Property="adonisExtensions:ScrollViewerExtension.HorizontalScrollBarPlacement"
Value="Docked" />
<Setter Property="HorizontalContentAlignment"
Value="Stretch" />
<LinearGradientBrush x:Key="FolderTabGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#FFE8C480" Offset="0" />
<GradientStop Color="#FFD9AA63" Offset="1" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="FolderBodyGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#FFF3D9A4" Offset="0" />
<GradientStop Color="#FFE5BD77" Offset="1" />
</LinearGradientBrush>
<DrawingImage x:Key="TreeFolderClosedImage">
<DrawingImage.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="{StaticResource FolderTabGradient}"
Geometry="M20 34 H54 L62 46 H108 C112 46 115 49 115 53 V60 H13 V45 C13 39 16 34 20 34 Z">
<GeometryDrawing.Pen>
<Pen Brush="#AC783F" Thickness="5" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Brush="{StaticResource FolderBodyGradient}"
Geometry="M13 60 H115 V104 C115 110 110 115 104 115 H24 C18 115 13 110 13 104 Z">
<GeometryDrawing.Pen>
<Pen Brush="#AC783F" Thickness="5" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="TreeFolderOpenImage">
<DrawingImage.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="{StaticResource FolderTabGradient}"
Geometry="M20 34 H54 L62 46 H103 C108 46 111 49 111 54 V105 H13 V45 C13 39 16 34 20 34 Z">
<GeometryDrawing.Pen>
<Pen Brush="#AC783F" Thickness="5" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Brush="{StaticResource FolderBodyGradient}"
Geometry="M30 65 H121 L104 108 C102 113 99 115 93 115 H24 C18 115 14 111 13 106 Z">
<GeometryDrawing.Pen>
<Pen Brush="#AC783F" Thickness="5" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<Style x:Key="FolderExpanderStyle" TargetType="ToggleButton">
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="20" />
<Setter Property="Focusable" Value="False" />
<Setter Property="ClickMode" Value="Press" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Background="Transparent">
<Viewbox x:Name="Arrow"
Tag="-90"
Width="16"
Height="16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5,0.5">
<Viewbox.RenderTransform>
<RotateTransform x:Name="ArrowRotation"
Angle="{Binding Tag, ElementName=Arrow}" />
</Viewbox.RenderTransform>
<Canvas Width="24"
Height="24">
<Path Fill="{TemplateBinding Foreground}"
Data="{StaticResource ArrowIcon}" />
</Canvas>
</Viewbox>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked"
Value="True">
<Setter TargetName="Arrow"
Property="Tag"
Value="-45" />
<Trigger.EnterActions>
<BeginStoryboard x:Name="ExpandArrow">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ArrowRotation"
Storyboard.TargetProperty="Angle"
From="-90"
To="-45"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard x:Name="CollapseArrow">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ArrowRotation"
Storyboard.TargetProperty="Angle"
From="-45"
To="-90"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<!-- Don't replay animation whe not needed -->
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<RemoveStoryboard BeginStoryboardName="ExpandArrow" />
<RemoveStoryboard BeginStoryboardName="CollapseArrow" />
</EventTrigger>
<EventTrigger RoutedEvent="FrameworkElement.Unloaded">
<RemoveStoryboard BeginStoryboardName="ExpandArrow" />
<RemoveStoryboard BeginStoryboardName="CollapseArrow" />
</EventTrigger>
<DataTrigger Binding="{Binding Folders.Count}"
Value="0">
<Setter Property="Visibility"
Value="Hidden" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="AssetsFolderListBox" TargetType="ListBox" BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
<Setter Property="VirtualizingPanel.ScrollUnit" Value="Item" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarExpansionMode" Value="NeverExpand" />
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarPlacement" Value="Docked" />
<Setter Property="adonisExtensions:ScrollViewerExtension.HorizontalScrollBarExpansionMode" Value="NeverExpand" />
<Setter Property="adonisExtensions:ScrollViewerExtension.HorizontalScrollBarPlacement" Value="Docked" />
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="HorizontalContentAlignment"
Value="Stretch" />
<Setter Property="ContextMenu"
Value="{StaticResource FolderContextMenu}" />
<Setter Property="Padding"
Value="0" />
<Setter Property="Margin"
Value="{Binding Indent}" />
</Style>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<HierarchicalDataTemplate ItemsSource="{Binding FoldersView, IsAsync=True}">
<StackPanel Orientation="Horizontal">
<Image x:Name="TreeImage"
Source="/FModel;component/Resources/folder.png"
Width="16"
Height="16"
HorizontalAlignment="Center"
Margin="0 0 3.5 0" />
<Border x:Name="TreeBadge"
CornerRadius="5"
Height="10"
MinWidth="10"
Background="#705542"
Margin="-10.5 0 4.5 0"
VerticalAlignment="Bottom">
<TextBlock Text="{Binding AssetsList.Assets.Count}"
Margin="2.5 0 2.5 0"
Padding="1 0 0 0"
FontSize="7"
Foreground="White"
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0"
Style="{StaticResource FolderExpanderStyle}"
IsChecked="{Binding IsExpanded, Mode=OneWay}" />
<StackPanel Grid.Column="1"
Grid.ColumnSpan="2"
Orientation="Horizontal"
Margin="0 3 0 3">
<Grid Width="16"
Height="16"
Margin="0 0 3.5 0"
VerticalAlignment="Center">
<Image x:Name="DefaultFolderIcon"
Width="16"
Height="14"
Visibility="Collapsed"
Source="{StaticResource TreeFolderClosedImage}" />
<Image x:Name="CustomFolderIcon"
Width="16"
Height="16"
Source="{Binding Header, Converter={x:Static converters:FolderTreeIconConverter.Instance}}" />
</Grid>
<Border x:Name="TreeBadge"
CornerRadius="5"
Height="10"
MinWidth="10"
Background="#705542"
Margin="-10.5 0 4.5 0"
VerticalAlignment="Bottom">
<TextBlock Text="{Binding AssetsList.Assets.Count}"
Margin="2.5 0 2.5 0"
Padding="1 0 0 0"
FontSize="7"
Foreground="White"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Border>
<TextBlock Text="{Binding Header}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Border>
<TextBlock Text="{Binding Header}"
HorizontalAlignment="Left">
<TextBlock.ToolTip>
<ToolTip>
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} has {1} folders and {2} packages">
<Binding Path="Header" />
<Binding Path="FoldersView.Count" />
<Binding Path="AssetsList.Assets.Count" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding FoldersView.Count}"
Value="0">
<Setter TargetName="TreeImage"
HorizontalAlignment="Left">
<TextBlock.ToolTip>
<ToolTip>
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} has {1} folders and {2} packages">
<Binding Path="Header" />
<Binding Path="Folders.Count" />
<Binding Path="AssetsList.Assets.Count" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
</StackPanel>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsExpanded}"
Value="True">
<Setter TargetName="DefaultFolderIcon"
Property="Source"
Value="/FModel;component/Resources/empty_folder.png" />
Value="{StaticResource TreeFolderOpenImage}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}"
Value="True">
<Setter TargetName="DefaultFolderIcon"
Property="Source"
Value="{StaticResource TreeFolderOpenImage}" />
</DataTrigger>
<DataTrigger Binding="{Binding Source, ElementName=CustomFolderIcon}"
Value="{x:Null}">
<Setter TargetName="CustomFolderIcon"
Property="Visibility"
Value="Collapsed" />
<Setter TargetName="DefaultFolderIcon"
Property="Visibility"
Value="Visible" />
</DataTrigger>
<DataTrigger Binding="{Binding AssetsList.Assets.Count}"
Value="0">
@@ -300,408 +482,10 @@
Property="Visibility"
Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Config">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/gear.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Plugins">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/puzzle.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Windows">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/windows.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Android">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/android.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="IOS">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/apple.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Mac">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/apple.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Linux">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/linux.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="LinuxAArch64">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/linux.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Unix">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/unix.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Weapons">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/weapon.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Localization">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/localization.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Internationalization">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/localization.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="UI">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/ui.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Sounds">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/sound.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="2dAssets">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/texture.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Textures">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/texture.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Engine">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/engine.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="EngineContent">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/engine.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Platforms">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/pc.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Fortnite">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/fortnite.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="FortniteGame">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/fortnitebr.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="WorldExplorers">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/battlebreakers.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="OakGame">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/borderlands.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="ShooterGame">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/valorant.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="RogueCompany">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/roguecompany.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="g3">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/spellbreak.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="SwGame">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/fallenorder.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Prospect">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/thecycle.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Materials">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/materialicon.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header, Converter={x:Static converters:CaseInsensitiveStringEqualsConverter.Instance}, ConverterParameter='Blueprints'}"
Value="True">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/blueprint.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Cinematics">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/cinematics.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Movies">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/cinematics.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Audio">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/sound.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Creative">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/creative.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header}"
Value="Athena">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/athena.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Header, Converter={x:Static converters:CaseInsensitiveStringEqualsConverter.Instance}, ConverterParameter='StateOfDecay2'}"
Value="True">
<Setter TargetName="TreeImage"
Property="Source"
Value="/FModel;component/Resources/stateofdecay2.png" />
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</DataTemplate.Triggers>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type TreeViewItem}"
BasedOn="{StaticResource TreeViewItemStyle}">
<Setter Property="HorizontalContentAlignment"
Value="Stretch" />
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="IsExpanded"
Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="ContextMenu"
Value="{StaticResource FolderContextMenu}" />
<Setter Property="controls:TreeViewItemBehavior.IsBroughtIntoViewWhenSelected"
Value="True" />
<Setter Property="Padding"
Value="5 3" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TreeViewItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Border x:Name="Border"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding adonisExtensions:CornerRadiusExtension.CornerRadius}" />
<Border x:Name="SpotlightLayer"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Background="{TemplateBinding adonisExtensions:CursorSpotlightExtension.BackgroundBrush}"
BorderBrush="{TemplateBinding adonisExtensions:CursorSpotlightExtension.BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding adonisExtensions:CornerRadiusExtension.CornerRadius}"
adonisExtensions:CursorSpotlightExtension.MouseEventSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TreeViewItem}}"
SnapsToDevicePixels="False" />
<ToggleButton x:Name="Expander"
Grid.Row="0"
Grid.Column="0"
Width="12"
Height="12"
Focusable="False"
ClickMode="Press"
Foreground="{TemplateBinding Foreground}"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
ContentTemplate="{DynamicResource {x:Static adonisUi:Templates.Expander}}"
RenderTransformOrigin="0.5, 0.5">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border Background="Transparent">
<Viewbox Width="16"
Height="16"
HorizontalAlignment="Center">
<Canvas Width="24"
Height="24">
<Path Fill="{TemplateBinding Foreground}"
Data="{StaticResource ArrowIcon}" />
</Canvas>
</Viewbox>
</Border>
</ControlTemplate>
</ToggleButton.Template>
<ToggleButton.Margin>
<MultiBinding Converter="{x:Static adonisConverters:ValuesToThicknessConverter.Instance}">
<Binding Path="Padding.Left"
RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Source="0" />
<Binding Source="8" />
<Binding Source="0" />
</MultiBinding>
</ToggleButton.Margin>
<ToggleButton.RenderTransform>
<RotateTransform x:Name="ExpanderRotateTransform"
Angle="-90" />
</ToggleButton.RenderTransform>
</ToggleButton>
<ContentPresenter x:Name="PART_Header"
Grid.Row="0"
Grid.Column="1"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}">
<ContentPresenter.Margin>
<MultiBinding Converter="{x:Static adonisConverters:ValuesToThicknessConverter.Instance}">
<Binding>
<Binding.Source>
<system:Double>-5</system:Double>
</Binding.Source>
</Binding>
<Binding Path="Padding.Top"
RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="Padding.Right"
RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="Padding.Bottom"
RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</ContentPresenter.Margin>
</ContentPresenter>
<ItemsPresenter x:Name="ItemsHost"
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded"
Value="False">
<Setter TargetName="ItemsHost"
Property="Visibility"
Value="Collapsed" />
</Trigger>
<Trigger Property="IsExpanded"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Angle"
Storyboard.TargetName="ExpanderRotateTransform"
Duration="00:00:0.2"
To="-45" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Angle"
Storyboard.TargetName="ExpanderRotateTransform"
Duration="00:00:0.2"
From="-45" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<Trigger Property="HasItems"
Value="False">
<Setter TargetName="Expander"
Property="Visibility"
Value="Hidden" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Items.Count, RelativeSource={RelativeSource Self}, FallbackValue=0}"
Value="0">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<TextBlock Text="No folders found in archives, make sure you loaded at least one"
FontWeight="SemiBold"
TextAlignment="Center"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ErrorBrush}}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="GameFilesTabControl" TargetType="TabControl" BasedOn="{StaticResource {x:Type TabControl}}">

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
@@ -124,47 +123,41 @@ public partial class SearchView
await CurrentViewModel?.CycleSortSizeMode();
}
private async void OnAssetDoubleClick(object sender, RoutedEventArgs e)
private void OnAssetDoubleClick(object sender, RoutedEventArgs e)
{
if (CurrentListView?.SelectedItem is not GameFile entry)
return;
await NavigateToAssetAndSelect(entry);
NavigateToAssetAndSelect(entry);
}
private async void OnGoToRefPackage(object sender, RoutedEventArgs e)
private void OnGoToRefPackage(object sender, RoutedEventArgs e)
{
if (_refViewModel.RefFile is not GameFile entry)
return;
await NavigateToAssetAndSelect(entry);
NavigateToAssetAndSelect(entry);
}
private async Task NavigateToAssetAndSelect(GameFile entry)
private void 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();
folder.SearchText = string.Empty;
folder.SelectedCategory = EAssetCategory.All;
do
{ await Task.Delay(100); } while (MainWindow.YesWeCats.AssetsListName.Items.Count < folder.AssetsList.Assets.Count);
var mainWindow = MainWindow.Instance;
mainWindow.Activate();
mainWindow.SelectFolder(folder);
while (!folder.IsSelected || MainWindow.YesWeCats.AssetsFolderName.SelectedItem != folder)
await Task.Delay(50); // stops assets tab from opening too early
var vm = folder.AssetsList.Assets.FirstOrDefault(x => x.Asset == entry);
if (vm == null)
return;
ApplicationService.ApplicationView.SelectedLeftTabIndex = 2; // assets tab
do
{
await Task.Delay(100);
var vm = MainWindow.YesWeCats.AssetsListName.Items
.OfType<GameFileViewModel>()
.FirstOrDefault(x => x.Asset == entry);
MainWindow.YesWeCats.AssetsListName.SelectedItem = vm;
MainWindow.YesWeCats.AssetsListName.ScrollIntoView(vm);
} while (MainWindow.YesWeCats.AssetsListName.SelectedItem == null);
mainWindow.SelectAsset(vm);
}
private async void OnAssetExtract(object sender, RoutedEventArgs e)
@@ -175,7 +168,7 @@ public partial class SearchView
WindowState = WindowState.Minimized;
await _threadWorkerView.Begin(cancellationToken => _applicationView.CUE4Parse.Extract(cancellationToken, entry, true));
MainWindow.YesWeCats.Activate();
MainWindow.Instance.Activate();
}
private void OnWindowKeyDown(object sender, KeyEventArgs e)