From dd174f5bf73a54d03251a0bebbc0a4ddd0d193a2 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Wed, 6 May 2026 21:43:32 +0200 Subject: [PATCH 01/26] code --- .../Containers/CachedPackFileContainer.cs | 25 ++ .../Models/Containers/PackFileContainer.cs | 34 ++ .../PackFiles/Models/IPackFileContainer.cs | 1 + .../CacheDatabase/CacheDbContext.cs | 1 + .../CachedPackFileContainerTests.cs | 148 ++++++++ .../Containers/PackFileContainerTests.cs | 124 +++++++ .../PackFileTree/PackFileBrowserViewModel.cs | 246 ++++++-------- .../BaseDialogs/PackFileTree/SearchFilter.cs | 317 +++++++++++++----- .../BaseDialogs/PackFileTree/TreeNode.cs | 268 ++++++++------- .../PackFileTree/TreeNodeSource.cs | 144 -------- .../PackFileBrowserViewModelTests.cs | 123 ++++++- 11 files changed, 915 insertions(+), 516 deletions(-) delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeSource.cs diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs index ee3bafa20..dd0a8c6f7 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs @@ -61,6 +61,31 @@ public bool ContainsFile(string path) return entries.Select(e => (e.RelativePath, ToPackFile(e))).ToList(); } + public List<(string Path, PackFile File)> SearchFiles(string? textFilter, IReadOnlyList? extensions) + { + using var db = new CacheDbContext(_dbOptions); + + IQueryable query = db.Files.AsNoTracking(); + + if (extensions != null && extensions.Count > 0) + { + // Filter by extensions using SQL — handle most common case of actual file extensions + var extList = extensions.Select(e => e.ToLowerInvariant()).ToList(); + query = query.Where(f => extList.Any(ext => f.FileName.Contains(ext))); + } + + if (!string.IsNullOrWhiteSpace(textFilter)) + { + // Use LIKE for case-insensitive substring matching in SQLite + var pattern = $"%{textFilter}%"; + query = query.Where(f => EF.Functions.Like(f.FileName, pattern)); + } + + var entries = query.OrderBy(f => f.RelativePath).ToList(); + + return entries.Select(e => (e.RelativePath, ToPackFile(e))).ToList(); + } + public Dictionary GetAllFiles() { using var db = new CacheDbContext(_dbOptions); diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs index 093fa3087..87ad5328d 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs @@ -74,6 +74,40 @@ public DirectoryContent GetDirectoryContent(string directoryPath) return output; } + public List<(string Path, PackFile File)> SearchFiles(string? textFilter, IReadOnlyList? extensions) + { + var results = new List<(string Path, PackFile File)>(); + + foreach (var (path, packFile) in FileList) + { + if (extensions != null && extensions.Count > 0) + { + var matchesExtension = false; + foreach (var ext in extensions) + { + if (packFile.Name.Contains(ext, StringComparison.OrdinalIgnoreCase)) + { + matchesExtension = true; + break; + } + } + if (!matchesExtension) + continue; + } + + if (!string.IsNullOrWhiteSpace(textFilter)) + { + if (!packFile.Name.Contains(textFilter, StringComparison.OrdinalIgnoreCase)) + continue; + } + + results.Add((path, packFile)); + } + + results.Sort((a, b) => StringComparer.OrdinalIgnoreCase.Compare(a.Path, b.Path)); + return results; + } + public PackFileContainer(string name) { Name = name; diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs index e20975ffc..ca34efe30 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs @@ -29,5 +29,6 @@ public interface IPackFileContainer List<(string FileName, PackFile Pack)> FindAllWithExtention(string extention); Dictionary GetAllFiles(); DirectoryContent GetDirectoryContent(string directoryPath); + List<(string Path, PackFile File)> SearchFiles(string? textFilter, IReadOnlyList? extensions); } } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/CacheDatabase/CacheDbContext.cs b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/CacheDatabase/CacheDbContext.cs index 03ef10bba..12b072a69 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/CacheDatabase/CacheDbContext.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/CacheDatabase/CacheDbContext.cs @@ -26,6 +26,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasIndex(e => e.RelativePath).IsUnique(); entity.HasIndex(e => e.Extension); entity.HasIndex(e => e.FolderPath); + entity.HasIndex(e => e.FileName); }); } } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs index 12dc99df7..856d76fe4 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs @@ -487,4 +487,152 @@ public void GetDirectoryContent_EmptyContainer_ReturnsEmpty() Assert.That(content.SubFolders, Is.Empty); } } + + internal class CachedPackFileContainer_SearchFiles + { + private string _tempDir; + private string _dbFilePath; + private CachedPackFileContainer _container; + + [SetUp] + public void Setup() + { + _tempDir = Path.Combine(Path.GetTempPath(), "CachedSearchTests_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + _dbFilePath = Path.Combine(_tempDir, "test.db"); + + var sourceContainer = new PackFileContainer("SearchTest") + { + IsCaPackFile = true, + SystemFilePath = @"c:\game\data" + }; + sourceContainer.SourcePackFilePaths.Add(@"c:\game\data\pack1.pack"); + + var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\pack1.pack" }; + sourceContainer.AddOrUpdateFile("models\\unit.rigid_model_v2", new PackFile("unit.rigid_model_v2", + new PackedFileSource(parent, 0, 100, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\vehicle.rigid_model_v2", new PackFile("vehicle.rigid_model_v2", + new PackedFileSource(parent, 100, 200, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\textures\\diffuse.dds", new PackFile("diffuse.dds", + new PackedFileSource(parent, 300, 50, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\textures\\normal.dds", new PackFile("normal.dds", + new PackedFileSource(parent, 350, 50, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("audio\\battle_sound.wem", new PackFile("battle_sound.wem", + new PackedFileSource(parent, 400, 300, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("audio\\music.wem", new PackFile("music.wem", + new PackedFileSource(parent, 700, 150, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("scripts\\campaign_script.lua", new PackFile("campaign_script.lua", + new PackedFileSource(parent, 850, 80, false, false, CompressionFormat.None, 0))); + + var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); + PackFileContainerCacheHelper.SaveCache("fp", sourceContainer, dbOptions); + _container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp")!; + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_tempDir)) + Directory.Delete(_tempDir, true); + } + + [Test] + public void NullFilters_ReturnsAllFiles() + { + var results = _container.SearchFiles(null, null); + Assert.That(results.Count, Is.EqualTo(7)); + } + + [Test] + public void TextFilter_MatchesFileName() + { + var results = _container.SearchFiles("unit", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("unit.rigid_model_v2")); + } + + [Test] + public void TextFilter_IsCaseInsensitive() + { + var results = _container.SearchFiles("UNIT", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("unit.rigid_model_v2")); + } + + [Test] + public void TextFilter_PartialMatch() + { + var results = _container.SearchFiles("battle", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); + } + + [Test] + public void TextFilter_NoMatch_ReturnsEmpty() + { + var results = _container.SearchFiles("nonexistent", null); + Assert.That(results, Is.Empty); + } + + [Test] + public void ExtensionFilter_SingleExtension() + { + var results = _container.SearchFiles(null, [".wem"]); + Assert.That(results.Count, Is.EqualTo(2)); + Assert.That(results.All(r => r.File.Name.EndsWith(".wem")), Is.True); + } + + [Test] + public void ExtensionFilter_MultipleExtensions() + { + var results = _container.SearchFiles(null, [".wem", ".lua"]); + Assert.That(results.Count, Is.EqualTo(3)); + } + + [Test] + public void ExtensionFilter_NoMatch_ReturnsEmpty() + { + var results = _container.SearchFiles(null, [".xyz"]); + Assert.That(results, Is.Empty); + } + + [Test] + public void CombinedFilters_TextAndExtension() + { + var results = _container.SearchFiles("battle", [".wem"]); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); + } + + [Test] + public void CombinedFilters_TextMatchesButExtensionDoesNot_ReturnsEmpty() + { + var results = _container.SearchFiles("unit", [".wem"]); + Assert.That(results, Is.Empty); + } + + [Test] + public void ResultsAreSortedByPath() + { + var results = _container.SearchFiles(null, null); + var paths = results.Select(r => r.Path).ToList(); + var sorted = paths.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToList(); + Assert.That(paths, Is.EqualTo(sorted)); + } + + [Test] + public void ResultsContainCorrectPaths() + { + var results = _container.SearchFiles("diffuse", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].Path, Is.EqualTo("models\\textures\\diffuse.dds")); + } + + [Test] + public void EmptyExtensionList_TreatedAsNoFilter() + { + var results = _container.SearchFiles(null, []); + Assert.That(results.Count, Is.EqualTo(7)); + } + } } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs index 6e0f02f38..3904f7320 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs @@ -492,4 +492,128 @@ public void SaveFileData_UpdatesDataSource() } } + + internal class PackFileContainer_SearchFiles + { + private PackFileContainer _container; + + [SetUp] + public void Setup() + { + _container = new PackFileContainer("Test"); + _container.AddOrUpdateFile("models\\unit.rigid_model_v2", new PackFile("unit.rigid_model_v2", null)); + _container.AddOrUpdateFile("models\\vehicle.rigid_model_v2", new PackFile("vehicle.rigid_model_v2", null)); + _container.AddOrUpdateFile("models\\textures\\diffuse.dds", new PackFile("diffuse.dds", null)); + _container.AddOrUpdateFile("models\\textures\\normal.dds", new PackFile("normal.dds", null)); + _container.AddOrUpdateFile("audio\\battle_sound.wem", new PackFile("battle_sound.wem", null)); + _container.AddOrUpdateFile("audio\\music.wem", new PackFile("music.wem", null)); + _container.AddOrUpdateFile("scripts\\campaign_script.lua", new PackFile("campaign_script.lua", null)); + } + + [Test] + public void NullFilters_ReturnsAllFiles() + { + var results = _container.SearchFiles(null, null); + Assert.That(results.Count, Is.EqualTo(7)); + } + + [Test] + public void TextFilter_MatchesFileName() + { + var results = _container.SearchFiles("unit", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("unit.rigid_model_v2")); + } + + [Test] + public void TextFilter_IsCaseInsensitive() + { + var results = _container.SearchFiles("UNIT", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("unit.rigid_model_v2")); + } + + [Test] + public void TextFilter_PartialMatch() + { + var results = _container.SearchFiles("battle", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); + } + + [Test] + public void TextFilter_NoMatch_ReturnsEmpty() + { + var results = _container.SearchFiles("nonexistent", null); + Assert.That(results, Is.Empty); + } + + [Test] + public void ExtensionFilter_SingleExtension() + { + var results = _container.SearchFiles(null, [".wem"]); + Assert.That(results.Count, Is.EqualTo(2)); + Assert.That(results.All(r => r.File.Name.EndsWith(".wem")), Is.True); + } + + [Test] + public void ExtensionFilter_MultipleExtensions() + { + var results = _container.SearchFiles(null, [".wem", ".lua"]); + Assert.That(results.Count, Is.EqualTo(3)); + } + + [Test] + public void ExtensionFilter_NoMatch_ReturnsEmpty() + { + var results = _container.SearchFiles(null, [".xyz"]); + Assert.That(results, Is.Empty); + } + + [Test] + public void CombinedFilters_TextAndExtension() + { + var results = _container.SearchFiles("battle", [".wem"]); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); + } + + [Test] + public void CombinedFilters_TextMatchesButExtensionDoesNot_ReturnsEmpty() + { + var results = _container.SearchFiles("unit", [".wem"]); + Assert.That(results, Is.Empty); + } + + [Test] + public void ResultsAreSortedByPath() + { + var results = _container.SearchFiles(null, null); + var paths = results.Select(r => r.Path).ToList(); + var sorted = paths.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToList(); + Assert.That(paths, Is.EqualTo(sorted)); + } + + [Test] + public void ResultsContainCorrectPaths() + { + var results = _container.SearchFiles("diffuse", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].Path, Is.EqualTo("models\\textures\\diffuse.dds")); + } + + [Test] + public void EmptyTextFilter_TreatedAsNull() + { + var results = _container.SearchFiles(" ", null); + Assert.That(results.Count, Is.EqualTo(7)); + } + + [Test] + public void EmptyExtensionList_TreatedAsNoFilter() + { + var results = _container.SearchFiles(null, []); + Assert.That(results.Count, Is.EqualTo(7)); + } + } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs index 7c1f9475c..b56fa09a9 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; @@ -22,24 +22,12 @@ namespace Shared.Ui.BaseDialogs.PackFileTree public partial class PackFileBrowserViewModel : ObservableObject, IDisposable, IDropTarget { - private sealed class PackFileTreeState - { - public TreeNodeSource RootSource { get; } - public TreeNode RootNode { get; } - - public PackFileTreeState(TreeNodeSource rootSource, TreeNode rootNode) - { - RootSource = rootSource; - RootNode = rootNode; - } - } - protected IPackFileService _packFileService; private readonly IEventHub? _eventHub; private readonly IWindowsKeyboard _windowKeyboard; private readonly ApplicationSettingsService _applicationSettingsService; private readonly IContextMenuBuilder _contextMenuBuilder; - private readonly Dictionary _treeStates = []; + private readonly Dictionary _treeRoots = []; public event FileSelectedDelegate FileOpen; public event NodeSelectedDelegate NodeSelected; @@ -72,7 +60,7 @@ public PackFileBrowserViewModel(ApplicationSettingsService applicationSettingsSe _eventHub?.Register(this, x => Database_PackFileFolderRenamed(x.Container, x.OldNodePath, x.NewNodePath)); _eventHub?.Register(this, ContainerSaved); - Filter = new SearchFilter(Files, () => _treeStates.Values.Select(x => x.RootSource)); + Filter = new SearchFilter(Files, () => _treeRoots.Values); Filter.ShowFoldersOnly = ShowFoldersOnly; foreach (var item in _packFileService.GetAllPackfileContainers()) { @@ -98,29 +86,24 @@ partial void OnSelectedItemChanged(TreeNode value) private void Database_PackFileFolderRemoved(IPackFileContainer container, string folder) { - var state = GetPackFileTreeState(container); - var root = state.RootNode; - var nodeToDelete = GetNodeFromPath(state.RootSource, folder, false); + var root = GetRootNode(container); + var nodeToDelete = GetNodeFromPath(root, folder, false); if (nodeToDelete == null) return; - var materializedFolder = nodeToDelete.MaterializedNode; - nodeToDelete.Parent?.RemoveChild(nodeToDelete); - if (materializedFolder != null) - { - materializedFolder.Parent?.Children.Remove(materializedFolder); - materializedFolder.RemoveSelf(); - } + var parentNode = nodeToDelete.Parent; + parentNode?.RemoveChild(nodeToDelete); + parentNode?.Children.Remove(nodeToDelete); + nodeToDelete.RemoveSelf(); - state.RootSource.UnsavedChanged = true; root.UnsavedChanged = true; Filter.Reapply(); } private void Database_PackFileFolderRenamed(IPackFileContainer container, string oldPath, string newPath) { - var state = GetPackFileTreeState(container); - var node = GetNodeFromPath(state.RootSource, oldPath, false); + var root = GetRootNode(container); + var node = GetNodeFromPath(root, oldPath, false); if (node == null) return; @@ -136,32 +119,28 @@ private void Database_PackFileFolderRenamed(IPackFileContainer container, string private void ContainerSaved(PackFileContainerSavedEvent e) { - var state = GetPackFileTreeState(e.Container); - var root = state.RootNode; + var root = GetRootNode(e.Container); - ClearUnsavedOnLoadedSourceNodes(state.RootSource); + ClearUnsavedOnLoadedNodes(root); - root.UnsavedChanged = false; root.ForeachNode((node) => node.UnsavedChanged = false); Filter.Reapply(); } - private static void ClearUnsavedOnLoadedSourceNodes(TreeNodeSource node) + private static void ClearUnsavedOnLoadedNodes(TreeNode node) { node.UnsavedChanged = false; if (!node.ChildrenLoaded) return; - foreach (var child in node.Children) - ClearUnsavedOnLoadedSourceNodes(child); + foreach (var child in node.BackingChildren) + ClearUnsavedOnLoadedNodes(child); } private void Database_PackFilesRemoved(IPackFileContainer container, List files) { - var state = GetPackFileTreeState(container); - var root = state.RootNode; + var root = GetRootNode(container); root.UnsavedChanged = true; - state.RootSource.UnsavedChanged = true; foreach (var file in files) { @@ -169,13 +148,10 @@ private void Database_PackFilesRemoved(IPackFileContainer container, List files) { - var state = GetPackFileTreeState(container); - var root = state.RootNode; + var root = GetRootNode(container); root.UnsavedChanged = true; - state.RootSource.UnsavedChanged = true; foreach (var item in files) { @@ -271,27 +243,28 @@ private void AddFiles(IPackFileContainer container, List files) var directoryEnd = fullPath.LastIndexOf(Path.DirectorySeparatorChar); - // Check if alreayd added - this happens moving files. - - TreeNodeSource newNode; + TreeNode newNode; if (numSeperators == 0) { - RemoveExistingFileNode(state.RootSource, item.Name, item); - newNode = new TreeNodeSource(item.Name, NodeType.File, container, state.RootSource, item); - InsertChildSorted(state.RootSource, newNode); + RemoveExistingFileNode(root, item.Name, item); + newNode = new TreeNode(item.Name, NodeType.File, container, root, item); + InsertChildSorted(root, newNode); } else { var directory = fullPath.Substring(0, directoryEnd); - var folder = GetNodeFromPath(state.RootSource, directory)!; - newNode = new TreeNodeSource(item.Name, NodeType.File, container, folder, item); + var folder = GetNodeFromPath(root, directory)!; + newNode = new TreeNode(item.Name, NodeType.File, container, folder, item); - // remove any existing files with same name - var existingFile = folder.Children.FirstOrDefault(node => node.Name == item.Name && node.NodeType == NodeType.File); + var existingFile = folder.BackingChildren.FirstOrDefault(node => node.Name == item.Name && node.NodeType == NodeType.File); if (existingFile != null) { folder.RemoveChild(existingFile); - existingFile.MaterializedNode?.RemoveSelf(); + if (folder.Children.Contains(existingFile)) + { + folder.Children.Remove(existingFile); + existingFile.RemoveSelf(); + } } InsertChildSorted(folder, newNode); @@ -299,7 +272,7 @@ private void AddFiles(IPackFileContainer container, List files) newNode.UnsavedChanged = true; var parent = newNode.Parent; - while (parent != null && parent != state.RootSource) + while (parent != null && parent != root) { parent.UnsavedChanged = true; parent = parent.Parent; @@ -311,74 +284,65 @@ private void AddFiles(IPackFileContainer container, List files) public TreeNode? GetFromPath(TreeNode parent, string path) { - if (parent.Source != null) - { - var sourceNode = GetAnyNodeFromPath(parent.Source, path); - if (sourceNode == null) - return null; + if (path.Length == 0) + return parent; - if (sourceNode.NodeType == NodeType.File) - return GetFromPathMaterialized(parent, path); + // First determine what the target is by searching through backing children + var target = FindInBackingChildren(parent, path); + if (target == null) + return null; - return sourceNode.MaterializedNode; - } + if (target.NodeType == NodeType.File) + return GetFromPathViaMaterialization(parent, path); - var numSeperators = path.Count(x => x == Path.DirectorySeparatorChar); + // Directory: only return if already materialized (visible in the WPF tree) + return FindInMaterializedChildren(parent, path); + } + + private static TreeNode? FindInBackingChildren(TreeNode parent, string path) + { if (path.Length == 0) return parent; - var nodeName = path; - var remainingStr = ""; - - if (numSeperators != 0) - { - var currentIndex = path.IndexOf(Path.DirectorySeparatorChar, 0); - nodeName = path.Substring(0, currentIndex); - remainingStr = path.Substring(currentIndex + 1); - } + parent.EnsureChildrenPopulated(); - foreach (var child in parent.Children) - { - if (child.Name == nodeName) - return GetFromPath(child, remainingStr); - } + var separatorIndex = path.IndexOf(Path.DirectorySeparatorChar); + var nodeName = separatorIndex == -1 ? path : path.Substring(0, separatorIndex); + var remainingPath = separatorIndex == -1 ? string.Empty : path.Substring(separatorIndex + 1); - return null; + var child = parent.BackingChildren.FirstOrDefault(x => x.Name == nodeName); + return child == null ? null : FindInBackingChildren(child, remainingPath); } - private TreeNode? GetFromPathMaterialized(TreeNode parent, string path) + private static TreeNode? GetFromPathViaMaterialization(TreeNode parent, string path) { if (path.Length == 0) return parent; + parent.MaterializeChildren(); + var separatorIndex = path.IndexOf(Path.DirectorySeparatorChar); var nodeName = separatorIndex == -1 ? path : path.Substring(0, separatorIndex); var remainingPath = separatorIndex == -1 ? string.Empty : path.Substring(separatorIndex + 1); - parent.EnsureChildrenLoaded(); var child = parent.Children.FirstOrDefault(x => x.Name == nodeName); - return child == null ? null : GetFromPathMaterialized(child, remainingPath); + return child == null ? null : GetFromPathViaMaterialization(child, remainingPath); } - private static TreeNodeSource? GetAnyNodeFromPath(TreeNodeSource parent, string path) + private static TreeNode? FindInMaterializedChildren(TreeNode parent, string path) { if (path.Length == 0) return parent; - parent.EnsureChildrenPopulated(); - var separatorIndex = path.IndexOf(Path.DirectorySeparatorChar); - if (separatorIndex == -1) - return parent.Children.FirstOrDefault(x => x.Name == path); - - var nodeName = path.Substring(0, separatorIndex); - var remainingPath = path.Substring(separatorIndex + 1); + var nodeName = separatorIndex == -1 ? path : path.Substring(0, separatorIndex); + var remainingPath = separatorIndex == -1 ? string.Empty : path.Substring(separatorIndex + 1); - var childDirectory = parent.Children.FirstOrDefault(x => x.Name == nodeName && x.NodeType == NodeType.Directory); - return childDirectory == null ? null : GetAnyNodeFromPath(childDirectory, remainingPath); + var child = parent.Children.FirstOrDefault(x => x.Name == nodeName); + return child == null ? null : FindInMaterializedChildren(child, remainingPath); } - private TreeNodeSource? GetNodeFromPath(TreeNodeSource parent, string path, bool createIfMissing = true) + private TreeNode? GetNodeFromPath(TreeNode parent, string path, bool createIfMissing = true) { var numSeperators = path.Count(x => x == Path.DirectorySeparatorChar); if (path.Length == 0) @@ -396,7 +360,7 @@ private void AddFiles(IPackFileContainer container, List files) remainingStr = path.Substring(currentIndex + 1); } - foreach (var child in parent.Children) + foreach (var child in parent.BackingChildren) { if (child.Name == nodeName && child.NodeType == NodeType.Directory) return GetNodeFromPath(child, remainingStr, createIfMissing); @@ -404,7 +368,7 @@ private void AddFiles(IPackFileContainer container, List files) if (createIfMissing) { - var newNode = new TreeNodeSource(nodeName, NodeType.Directory, parent.FileOwner, parent); + var newNode = new TreeNode(nodeName, NodeType.Directory, parent.FileOwner, parent, () => Filter.HasActiveFilter); newNode.MarkChildrenLoaded(); InsertChildSorted(parent, newNode); return GetNodeFromPath(newNode, remainingStr, createIfMissing); @@ -412,10 +376,9 @@ private void AddFiles(IPackFileContainer container, List files) return null; } - private TreeNodeSource? GetNodeFromPackFile(IPackFileContainer container, PackFile pf, bool createIfMissing = true) + private TreeNode? GetNodeFromPackFile(IPackFileContainer container, PackFile pf, bool createIfMissing = true) { - var state = GetPackFileTreeState(container); - var root = state.RootSource; + var root = GetRootNode(container); var fullPath = _packFileService.GetFullPath(pf, container); var numSeperators = fullPath.Count(x => x == Path.DirectorySeparatorChar); @@ -423,7 +386,7 @@ private void AddFiles(IPackFileContainer container, List files) if (numSeperators == 0) { - return root.Children.FirstOrDefault(x => x.Item == pf); + return root.BackingChildren.FirstOrDefault(x => x.Item == pf); } else { @@ -431,39 +394,37 @@ private void AddFiles(IPackFileContainer container, List files) var directory = fullPath.Substring(0, directoryEnd); var parent = GetNodeFromPath(root, directory, createIfMissing); - return parent?.Children.FirstOrDefault(x => x.Item == pf); + return parent?.BackingChildren.FirstOrDefault(x => x.Item == pf); } } private void ReloadTree(IPackFileContainer container) { - if (_treeStates.TryGetValue(container, out var existingState)) + if (_treeRoots.TryGetValue(container, out var existingRoot)) { - Files.Remove(existingState.RootNode); - _treeStates.Remove(container); + Files.Remove(existingRoot); + _treeRoots.Remove(container); } var skipWemFiles = container.IsCaPackFile && _applicationSettingsService.CurrentSettings.ShowCAWemFiles == false; - var rootSource = new TreeNodeSource(container.Name, NodeType.Root, container, null); - rootSource.SetChildLoader(node => LoadChildrenFromContainer(node, container, skipWemFiles)); - - var root = CreateTreeNode(rootSource, null); + var root = new TreeNode(container.Name, NodeType.Root, container, null, () => Filter.HasActiveFilter); + root.SetChildLoader(node => LoadChildrenFromContainer(node, container, skipWemFiles)); root.IsMainEditabelPack = _packFileService.GetEditablePack() == container; - _treeStates[container] = new PackFileTreeState(rootSource, root); + _treeRoots[container] = root; Files.Add(root); Filter.Reapply(); } - private bool LoadChildrenFromContainer(TreeNodeSource node, IPackFileContainer container, bool skipWemFiles) + private bool LoadChildrenFromContainer(TreeNode node, IPackFileContainer container, bool skipWemFiles) { var directoryPath = node.GetFullPath(); var content = container.GetDirectoryContent(directoryPath); foreach (var folderName in content.SubFolders) { - var childNode = new TreeNodeSource(folderName, NodeType.Directory, container, node); + var childNode = new TreeNode(folderName, NodeType.Directory, container, node, () => Filter.HasActiveFilter); childNode.SetChildLoader(n => LoadChildrenFromContainer(n, container, skipWemFiles)); node.AddChild(childNode); } @@ -473,7 +434,7 @@ private bool LoadChildrenFromContainer(TreeNodeSource node, IPackFileContainer c if (skipWemFiles && fileName.EndsWith(".wem", StringComparison.OrdinalIgnoreCase)) continue; - var fileNode = new TreeNodeSource(fileName, NodeType.File, container, node, file); + var fileNode = new TreeNode(fileName, NodeType.File, container, node, file); node.AddChild(fileNode); } @@ -482,27 +443,25 @@ private bool LoadChildrenFromContainer(TreeNodeSource node, IPackFileContainer c private void PackFileContainerRemoved(PackFileContainerRemovedEvent e) { - if (_treeStates.TryGetValue(e.Container, out var state)) + if (_treeRoots.TryGetValue(e.Container, out var root)) { - Files.Remove(state.RootNode); - _treeStates.Remove(e.Container); + Files.Remove(root); + _treeRoots.Remove(e.Container); } } - - public bool AllowDrop(TreeNode node, TreeNode targetNode = null) { - if (node.Item == null) // dragging a folder not supported + if (node.Item == null) return false; - if (node.FileOwner != targetNode.FileOwner) // dragging between different packs not supported + if (node.FileOwner != targetNode.FileOwner) return false; - if (node.FileOwner.IsCaPackFile) // dragging inside CA pack not supported + if (node.FileOwner.IsCaPackFile) return false; - if (targetNode.Item != null) // dragging file onto a file not supported + if (targetNode.Item != null) return false; return true; @@ -523,17 +482,12 @@ public bool Drop(TreeNode node, TreeNode targeNode) return true; } - private PackFileTreeState GetPackFileTreeState(IPackFileContainer container) - { - return _treeStates[container]; - } - - private TreeNode CreateTreeNode(TreeNodeSource source, TreeNode? parent) + private TreeNode GetRootNode(IPackFileContainer container) { - return new TreeNode(source, parent, CreateTreeNode, () => Filter.HasActiveFilter); + return _treeRoots[container]; } - private static readonly Comparison TreeNodeSourceComparison = (left, right) => + private static readonly Comparison TreeNodeComparison = (left, right) => { var nodeTypeComparison = left.NodeType.CompareTo(right.NodeType); if (nodeTypeComparison != 0) @@ -542,22 +496,26 @@ private TreeNode CreateTreeNode(TreeNodeSource source, TreeNode? parent) return StringComparer.CurrentCultureIgnoreCase.Compare(left.Name, right.Name); }; - private static void InsertChildSorted(TreeNodeSource parent, TreeNodeSource child) + private static void InsertChildSorted(TreeNode parent, TreeNode child) { parent.EnsureChildrenPopulated(); parent.AddChild(child); - parent.Children.Sort(TreeNodeSourceComparison); + parent.BackingChildren.Sort(TreeNodeComparison); } - private static void RemoveExistingFileNode(TreeNodeSource parent, string fileName, PackFile packFile) + private static void RemoveExistingFileNode(TreeNode parent, string fileName, PackFile packFile) { parent.EnsureChildrenPopulated(); - var existingFile = parent.Children.FirstOrDefault(node => node.NodeType == NodeType.File && (node.Item == packFile || node.Name == fileName)); + var existingFile = parent.BackingChildren.FirstOrDefault(node => node.NodeType == NodeType.File && (node.Item == packFile || node.Name == fileName)); if (existingFile == null) return; parent.RemoveChild(existingFile); - existingFile.MaterializedNode?.RemoveSelf(); + if (parent.Children.Contains(existingFile)) + { + parent.Children.Remove(existingFile); + existingFile.RemoveSelf(); + } } } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/SearchFilter.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/SearchFilter.cs index 31948142b..a6d8f1012 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/SearchFilter.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/SearchFilter.cs @@ -1,20 +1,27 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; -using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using Shared.Core.Misc; +using Shared.Core.PackFiles.Models; namespace Shared.Ui.BaseDialogs.PackFileTree { public class SearchFilter : NotifyPropertyChangedImpl, IDataErrorInfo { public string Error { get; set; } = string.Empty; - public string this[string columnName] => Filter(FilterText); + public string this[string columnName] => ApplyFilter(FilterText); private readonly ObservableCollection _nodeCollection; - private readonly Func> _sourceRootsFactory; + private readonly Func> _rootNodesFactory; + private readonly List _nodesPopulatedByFilter = []; + + private CancellationTokenSource? _debounceCts; + private const int DebounceMilliseconds = 250; + private bool _useDebounce; string _filterText = ""; public string FilterText @@ -23,10 +30,19 @@ public string FilterText set { SetAndNotify(ref _filterText, value); - Filter(_filterText); + if (_useDebounce) + DebounceFilter(); + else + ApplyFilter(_filterText); } } + public bool UseDebounce + { + get => _useDebounce; + set => _useDebounce = value; + } + private bool _showFoldersOnly; public bool ShowFoldersOnly { @@ -34,62 +50,84 @@ public bool ShowFoldersOnly set { SetAndNotify(ref _showFoldersOnly, value); - Filter(FilterText); + ApplyFilter(FilterText); } } - List _extensionFilter; + List? _extensionFilter; public int AutoExapandResultsAfterLimitedCount { get; set; } = 25; public bool HasActiveFilter => !string.IsNullOrWhiteSpace(FilterText) || ShowFoldersOnly || (_extensionFilter?.Count > 0); public Action? FilterCleared { get; set; } - internal SearchFilter(ObservableCollection nodes, Func> sourceRootsFactory) + internal SearchFilter(ObservableCollection nodes, Func> rootNodesFactory) { _nodeCollection = nodes; - _sourceRootsFactory = sourceRootsFactory; + _rootNodesFactory = rootNodesFactory; } - string Filter(string text) + private async void DebounceFilter() { - Regex expression = null; + _debounceCts?.Cancel(); + _debounceCts = new CancellationTokenSource(); + var token = _debounceCts.Token; + try { - expression = new Regex(text, RegexOptions.Compiled | RegexOptions.IgnoreCase); + await Task.Delay(DebounceMilliseconds, token); + if (!token.IsCancellationRequested) + ApplyFilter(FilterText); } - catch (Exception e) + catch (TaskCanceledException) { - return e.Message; + // Debounce cancelled — next keystroke will restart } + } - var rootSources = _sourceRootsFactory().ToList(); + string ApplyFilter(string text) + { + var rootNodes = _rootNodesFactory().ToList(); + + // Reset any nodes we force-populated in a previous filter pass + ResetFilterPopulatedNodes(); + + // Update child visibility predicate on all roots + Func? childPredicate = ShowFoldersOnly + ? (child => child.NodeType != NodeType.File) + : null; + foreach (var rootNode in rootNodes) + SetChildVisibilityPredicateRecursive(rootNode, childPredicate); - // Force-populate all source children when an active filter requires full traversal if (HasActiveFilter) { - foreach (var item in rootSources) - item.EnsureFullyPopulated(); - } + var textFilter = string.IsNullOrWhiteSpace(text) ? null : text; + var hasSearchFilter = textFilter != null || (_extensionFilter?.Count > 0); - foreach (var item in rootSources) - HasChildWithFilterMatch(item, expression); + if (hasSearchFilter) + { + foreach (var rootNode in rootNodes) + { + var container = rootNode.FileOwner; + var matchingFiles = container.SearchFiles(textFilter, _extensionFilter); - if (ShowFoldersOnly) - { - foreach (var node in rootSources) - ApplyFoldersOnlyFilter(node); - } + RebuildTreeFromSearchResults(rootNode, matchingFiles); + } + } - foreach (var rootNode in _nodeCollection) - rootNode.RefreshLoadedBranch(); + if (ShowFoldersOnly) + { + foreach (var rootNode in rootNodes) + ApplyFoldersOnlyFilter(rootNode); + } - if (AutoExapandResultsAfterLimitedCount != -1) - { - var visibleNodes = 0; - foreach (var item in rootSources) - visibleNodes += CountVisibleNodes(item); + foreach (var rootNode in _nodeCollection) + rootNode.RefreshLoadedBranch(); - if (HasActiveFilter) + if (AutoExapandResultsAfterLimitedCount != -1) { + var visibleNodes = 0; + foreach (var item in rootNodes) + visibleNodes += CountVisibleNodes(item); + foreach (var item in _nodeCollection) { item.ExpandForFilter(); @@ -102,16 +140,23 @@ string Filter(string text) item.ExpandIfVisible(markAsFilterExpansion: true); } } - else - { - foreach (var item in _nodeCollection) - { - item.AbsorbFilterExpansion(); - item.NormalizeLazyState(); - } + } + else + { + // Filter cleared — restore lazy tree state + foreach (var rootNode in rootNodes) + RestoreFullTree(rootNode); + + foreach (var rootNode in _nodeCollection) + rootNode.RefreshLoadedBranch(); - FilterCleared?.Invoke(); + foreach (var item in _nodeCollection) + { + item.AbsorbFilterExpansion(); + item.NormalizeLazyState(); } + + FilterCleared?.Invoke(); } return ""; @@ -119,82 +164,170 @@ string Filter(string text) public void Reapply() { - Filter(FilterText); + ApplyFilter(FilterText); } - private static void ApplyFoldersOnlyFilter(TreeNodeSource node) + public void SetExtensions(List extentions) { - if (node.NodeType == NodeType.File) - node.IsVisible = false; - else + _extensionFilter = extentions; + ApplyFilter(FilterText); + } + + private void RebuildTreeFromSearchResults(TreeNode rootNode, List<(string Path, PackFile File)> matchingFiles) + { + // Mark all currently-loaded nodes as not visible + SetVisibilityRecursive(rootNode, false); + rootNode.IsVisible = true; + + if (matchingFiles.Count == 0) + return; + + // Build the visible tree directly from paths — no container calls + foreach (var (path, file) in matchingFiles) { - node.IsVisible = true; - foreach (var child in node.Children) - ApplyFoldersOnlyFilter(child); + MarkPathVisibleFromData(rootNode, path, file); } } - private static int CountVisibleNodes(TreeNodeSource file) + private void MarkPathVisibleFromData(TreeNode rootNode, string filePath, PackFile file) { - if (file.NodeType == NodeType.File && file.IsVisible) - return 1; + var current = rootNode; + var segments = filePath.Split('\\'); - var count = 0; - foreach (var child in file.Children) - count += CountVisibleNodes(child); + // Navigate directory segments, creating nodes from path data if not loaded + for (var i = 0; i < segments.Length - 1; i++) + { + PopulateDirectoryIfNeeded(current, rootNode.FileOwner); + current.IsVisible = true; - return count; + var segmentName = segments[i]; + var child = current.BackingChildren.FirstOrDefault( + c => c.Name.Equals(segmentName, StringComparison.OrdinalIgnoreCase) && c.NodeType == NodeType.Directory); + + if (child == null) + { + // Create the directory node from path data + child = new TreeNode(segmentName, NodeType.Directory, rootNode.FileOwner, current); + current.AddChild(child); + } + + current = child; + } + + // Handle the final directory containing the file + PopulateDirectoryIfNeeded(current, rootNode.FileOwner); + current.IsVisible = true; + + // Find or create the file node + var fileName = segments[^1]; + var fileNode = current.BackingChildren.FirstOrDefault( + c => c.Name.Equals(fileName, StringComparison.OrdinalIgnoreCase) && c.NodeType == NodeType.File); + + if (fileNode == null) + { + fileNode = new TreeNode(fileName, NodeType.File, rootNode.FileOwner, current, file); + current.AddChild(fileNode); + } + + fileNode.IsVisible = true; } - public void SetExtensions(List extentions) + /// + /// If this node hasn't been populated yet (either by lazy-load or by a previous filter pass), + /// mark it as populated so we can add children to it. Track it for reset on filter clear. + /// + private void PopulateDirectoryIfNeeded(TreeNode node, IPackFileContainer container) { - _extensionFilter = extentions; - Filter(FilterText); + if (node.ChildrenLoaded) + { + // Already loaded (either from container or previous filter) — hide new children by default + foreach (var child in node.BackingChildren) + { + if (child.IsVisible) + continue; // already processed in a previous iteration + } + return; + } + + // Mark as loaded so we can add children directly. Track for reset. + node.MarkChildrenLoaded(); + _nodesPopulatedByFilter.Add(node); } - private bool HasChildWithFilterMatch(TreeNodeSource file, Regex expression) + /// + /// Resets nodes that were force-populated during filtering back to unloaded state. + /// This ensures the normal lazy-load from container works correctly when the filter changes/clears. + /// + private void ResetFilterPopulatedNodes() { - if (file.NodeType == NodeType.Root && file.Children.Count == 0) + foreach (var node in _nodesPopulatedByFilter) { - file.IsVisible = true; - return true; + node.BackingChildren.Clear(); + node.ResetChildrenLoaded(); } - if (file.NodeType == NodeType.File) + _nodesPopulatedByFilter.Clear(); + } + + private static void SetVisibilityRecursive(TreeNode node, bool visible) + { + node.IsVisible = visible; + if (node.ChildrenLoaded) { - var hasValidExtention = true; - if (_extensionFilter != null) - { - hasValidExtention = false; - foreach (var extention in _extensionFilter) - { - if (file.Name.Contains(extention)) - { - hasValidExtention = true; - continue; - } - } - } + foreach (var child in node.BackingChildren) + SetVisibilityRecursive(child, visible); + } + } - if (hasValidExtention) - { - if (expression.IsMatch(file.Name)) - { - file.IsVisible = true; - return true; - } - } + private static void RestoreFullTree(TreeNode node) + { + node.IsVisible = true; + if (node.ChildrenLoaded) + { + foreach (var child in node.BackingChildren) + RestoreFullTree(child); } + } - var hasChildMatch = false; - foreach (var child in file.Children) + private static void ApplyFoldersOnlyFilter(TreeNode node) + { + if (node.NodeType == NodeType.File) { - if (HasChildWithFilterMatch(child, expression)) - hasChildMatch = true; + node.IsVisible = false; + return; } - file.IsVisible = hasChildMatch; - return hasChildMatch; + node.IsVisible = true; + if (node.ChildrenLoaded) + { + foreach (var child in node.BackingChildren) + ApplyFoldersOnlyFilter(child); + } + } + + private static int CountVisibleNodes(TreeNode node) + { + if (node.NodeType == NodeType.File && node.IsVisible) + return 1; + + var count = 0; + if (node.ChildrenLoaded) + { + foreach (var child in node.BackingChildren) + count += CountVisibleNodes(child); + } + + return count; + } + + private static void SetChildVisibilityPredicateRecursive(TreeNode node, Func? predicate) + { + node.SetChildVisibilityPredicate(predicate); + if (node.ChildrenLoaded) + { + foreach (var child in node.BackingChildren) + SetChildVisibilityPredicateRecursive(child, predicate); + } } } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs index bec939cc1..d946527ba 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -15,22 +15,22 @@ public enum NodeType File } - /// - /// Represents the materialized UI node shown in the pack-file tree. - /// This class is optimized for WPF binding/state (expanded, visible, selected, unsaved changes) - /// and can be created lazily from a when branches are expanded or queried. - /// public partial class TreeNode : ObservableObject { - private readonly TreeNodeSource? _source; - private readonly Func? _nodeFactory; - private readonly Func? _isFilterActive; + private Func? _childLoader; + private bool _childrenLoaded; private bool _isExpandedByFilter; private readonly bool _isPlaceholder; + private readonly Func? _isFilterActive; + private Func? _childVisibilityPredicate; public IPackFileContainer FileOwner { get; private set; } public PackFile? Item { get; set; } public TreeNode? Parent { get; set; } + public List BackingChildren { get; } = []; + + public bool HasChildren => BackingChildren.Count > 0 || (!_childrenLoaded && _childLoader != null && NodeType != NodeType.File); + public bool ChildrenLoaded => _childrenLoaded; [ObservableProperty] public partial ObservableCollection Children { get; set; } = []; [ObservableProperty] public partial bool UnsavedChanged { get; set; } @@ -40,14 +40,13 @@ public partial class TreeNode : ObservableObject [ObservableProperty] public partial bool IsNodeExpanded { get; set; } = false; [ObservableProperty] public partial NodeType NodeType { get; private set; } - internal TreeNodeSource? Source => _source; internal bool HasMaterializedChildren => Children.Any(x => x._isPlaceholder == false); - public TreeNode(string name, NodeType type, IPackFileContainer ower, TreeNode? parent, PackFile? packFile = null) + public TreeNode(string name, NodeType type, IPackFileContainer owner, TreeNode? parent, PackFile? packFile = null) { Name = name; Item = packFile; - FileOwner = ower; + FileOwner = owner; Parent = parent; NodeType = type; @@ -55,21 +54,17 @@ public TreeNode(string name, NodeType type, IPackFileContainer ower, TreeNode? p throw new Exception($"Packfile name or folder is empty '{GetFullPath()}', this is not allowed! Please report as a bug if it happens outside of packfile loading! If it happens while loading clean up the packfile in RPFM"); } - internal TreeNode(TreeNodeSource source, TreeNode? parent, Func nodeFactory, Func isFilterActive) + internal TreeNode(string name, NodeType type, IPackFileContainer owner, TreeNode? parent, Func isFilterActive, PackFile? packFile = null) { - _source = source; - _nodeFactory = nodeFactory; _isFilterActive = isFilterActive; - - Name = source.Name; - Item = source.Item; - FileOwner = source.FileOwner; + Name = name; + Item = packFile; + FileOwner = owner; Parent = parent; - NodeType = source.NodeType; - UnsavedChanged = source.UnsavedChanged; - IsVisible = source.IsVisible; + NodeType = type; - source.MaterializedNode = this; + if (string.IsNullOrWhiteSpace(name)) + throw new Exception($"Packfile name or folder is empty '{GetFullPath()}', this is not allowed! Please report as a bug if it happens outside of packfile loading! If it happens while loading clean up the packfile in RPFM"); if (ShouldUsePlaceholder()) AddPlaceholderChild(); @@ -85,11 +80,55 @@ private TreeNode(TreeNode parent) IsVisible = false; } - public string GetFullPath() + public void SetChildLoader(Func childLoader) + { + _childLoader = childLoader; + } + + public void SetChildVisibilityPredicate(Func? predicate) + { + _childVisibilityPredicate = predicate; + } + + public void MarkChildrenLoaded() + { + _childrenLoaded = true; + } + + public void ResetChildrenLoaded() + { + _childrenLoaded = false; + } + + public void EnsureChildrenPopulated() + { + if (_childrenLoaded || _childLoader == null || NodeType == NodeType.File) + return; + + _childrenLoaded = _childLoader(this); + } + + public void EnsureFullyPopulated() + { + EnsureChildrenPopulated(); + foreach (var child in BackingChildren) + child.EnsureFullyPopulated(); + } + + public void AddChild(TreeNode child) + { + child.Parent = this; + BackingChildren.Add(child); + } + + public void RemoveChild(TreeNode child) { - if (_source != null) - return _source.GetFullPath(); + BackingChildren.Remove(child); + child.Parent = null; + } + public string GetFullPath() + { if (NodeType == NodeType.Root) return ""; @@ -111,33 +150,36 @@ public string GetFullPath() public List GetAllChildFileNodes() { - if (_source != null) + return EnumerateFileNodesDepthFirst().ToList(); + } + + public IEnumerable EnumerateAllNodesDepthFirst() + { + var stack = new Stack(); + stack.Push(this); + + while (stack.Count > 0) { - return _source.EnumerateFileNodesDepthFirst() - .Select(sourceNode => sourceNode.MaterializedNode ?? _nodeFactory!(sourceNode, null)) - .ToList(); - } + var current = stack.Pop(); + current.EnsureChildrenPopulated(); + yield return current; - var output = new List(); + for (var i = current.BackingChildren.Count - 1; i >= 0; i--) + stack.Push(current.BackingChildren[i]); + } + } - var nodes = new Stack(new[] { this }); - while (nodes.Any()) + public IEnumerable EnumerateFileNodesDepthFirst() + { + foreach (var node in EnumerateAllNodesDepthFirst()) { - var node = nodes.Pop(); if (node.NodeType == NodeType.File) - output.Add(node); - - foreach (var n in node.Children) - nodes.Push(n); + yield return node; } - return output; } public void RemoveSelf() { - if (_source != null) - _source.MaterializedNode = null; - foreach (var child in Children) child.RemoveSelf(); @@ -166,7 +208,7 @@ public void ExpandIfVisible(bool includeChildren = true, bool markAsFilterExpans else IsNodeExpanded = true; - EnsureChildrenLoaded(); + MaterializeChildren(); if (includeChildren) { foreach (var child in Children) @@ -182,46 +224,26 @@ public void ExpandIfVisible(bool includeChildren = true, bool markAsFilterExpans public TreeNode AddDirectoryChild(string name) { - var newNode = new TreeNode(name, NodeType.Directory, FileOwner, this); - - if (_source == null) - { - Children.Add(newNode); - return newNode; - } - - _source.EnsureChildrenPopulated(); - var sourceNode = new TreeNodeSource(name, NodeType.Directory, FileOwner, _source); - sourceNode.MarkChildrenLoaded(); - _source.AddChild(sourceNode); + EnsureChildrenPopulated(); + var newNode = new TreeNode(name, NodeType.Directory, FileOwner, this, _isFilterActive ?? (() => false)); + newNode.MarkChildrenLoaded(); + InsertChildSorted(this, newNode); if (HasMaterializedChildren || IsNodeExpanded || (_isFilterActive?.Invoke() ?? false)) - EnsureChildrenLoaded(); + MaterializeChildren(); if (Children.Count == 1 && Children[0]._isPlaceholder) - EnsureChildrenLoaded(); - - return sourceNode.MaterializedNode ?? newNode; - } + MaterializeChildren(); - internal void SyncFromSource() - { - if (_source == null) - return; - - Name = _source.Name; - UnsavedChanged = _source.UnsavedChanged; - IsVisible = _source.IsVisible; + return newNode; } internal void RefreshLoadedBranch() { - SyncFromSource(); - if (!HasMaterializedChildren) return; - EnsureChildrenLoaded(); + MaterializeChildren(); foreach (var child in Children) { if (child._isPlaceholder) @@ -231,52 +253,51 @@ internal void RefreshLoadedBranch() } } - internal void EnsureChildrenLoaded() + internal void MaterializeChildren() { - if (_source == null || _nodeFactory == null) - return; + EnsureChildrenPopulated(); - _source.EnsureChildrenPopulated(); - - if (!_source.HasChildren) + if (!HasChildren) return; - var existingChildren = Children - .Where(x => x._isPlaceholder == false && x._source != null) - .ToDictionary(x => x._source!, x => x); + // Propagate the visibility predicate to newly loaded children + if (_childVisibilityPredicate != null) + { + foreach (var child in BackingChildren) + { + if (child._childVisibilityPredicate != _childVisibilityPredicate) + child._childVisibilityPredicate = _childVisibilityPredicate; + } + } - var childSources = _source.Children + var childNodes = BackingChildren .Where(ShouldMaterializeChild) .ToList(); - foreach (var removedChild in Children.Where(x => x._isPlaceholder == false && x._source != null && childSources.Contains(x._source) == false).ToList()) - { - removedChild.RemoveSelf(); - } - Children.Clear(); - foreach (var childSource in childSources) + foreach (var child in childNodes) { - if (!existingChildren.TryGetValue(childSource, out var childNode)) - childNode = _nodeFactory(childSource, this); + child.Parent = this; + Children.Add(child); - childNode.Parent = this; - childNode.SyncFromSource(); - Children.Add(childNode); + if (child.IsNodeExpanded && !child.HasMaterializedChildren) + child.MaterializeChildren(); } if (Children.Count == 0 && ShouldUsePlaceholder()) AddPlaceholderChild(); } - internal void NormalizeLazyState() + internal void EnsureChildrenLoaded() { - if (_source == null) - return; + MaterializeChildren(); + } + internal void NormalizeLazyState() + { if (IsNodeExpanded) { - EnsureChildrenLoaded(); + MaterializeChildren(); foreach (var child in Children) { if (child._isPlaceholder) @@ -331,40 +352,16 @@ internal void AbsorbFilterExpansion() partial void OnIsNodeExpandedChanged(bool value) { - if (_source == null) - return; - if (value) - EnsureChildrenLoaded(); + MaterializeChildren(); else UnloadChildren(); LogLoadedNodeCount(value); } - partial void OnNameChanged(string value) - { - if (_source != null && !_isPlaceholder) - _source.Name = value; - } - - partial void OnUnsavedChangedChanged(bool value) - { - if (_source != null && !_isPlaceholder) - _source.UnsavedChanged = value; - } - - partial void OnIsVisibleChanged(bool value) - { - if (_source != null && !_isPlaceholder) - _source.IsVisible = value; - } - private void UnloadChildren() { - if (_source == null) - return; - foreach (var child in Children) child.RemoveSelf(); @@ -374,17 +371,20 @@ private void UnloadChildren() AddPlaceholderChild(); } - private bool ShouldMaterializeChild(TreeNodeSource childSource) + private bool ShouldMaterializeChild(TreeNode child) { if (_isFilterActive?.Invoke() != true) - return true; + return _childVisibilityPredicate?.Invoke(child) ?? true; + + if (_childVisibilityPredicate != null && !_childVisibilityPredicate(child)) + return false; - return childSource.IsVisible || childSource.MaterializedNode != null; + return child.IsVisible; } private bool ShouldUsePlaceholder() { - return _source != null && _source.HasChildren && !IsNodeExpanded && (_isFilterActive?.Invoke() != true); + return HasChildren && !IsNodeExpanded && (_isFilterActive?.Invoke() != true); } private void AddPlaceholderChild() @@ -420,5 +420,21 @@ private void LogLoadedNodeCount(bool expanded) var action = expanded ? "expanded" : "collapsed"; Console.WriteLine($"[PackFileTree] Node '{Name}' {action}. Loaded nodes: {loadedNodes}"); } + + private static readonly Comparison ChildComparison = (left, right) => + { + var nodeTypeComparison = left.NodeType.CompareTo(right.NodeType); + if (nodeTypeComparison != 0) + return nodeTypeComparison; + + return StringComparer.CurrentCultureIgnoreCase.Compare(left.Name, right.Name); + }; + + private static void InsertChildSorted(TreeNode parent, TreeNode child) + { + parent.EnsureChildrenPopulated(); + parent.AddChild(child); + parent.BackingChildren.Sort(ChildComparison); + } } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeSource.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeSource.cs deleted file mode 100644 index 118ed45a4..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeSource.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Collections.Generic; -using Shared.Core.PackFiles.Models; - -namespace Shared.Ui.BaseDialogs.PackFileTree -{ - /// - /// Represents the lightweight backing data model for the pack-file tree. - /// It stores the canonical hierarchy and metadata independent of UI state, and may optionally - /// reference a materialized when that branch has been created for display. - /// Children are loaded lazily from the container on first access of a directory/root node. - /// - internal sealed class TreeNodeSource - { - private Func? _childLoader; - private bool _childrenLoaded; - - public string Name { get; set; } - public NodeType NodeType { get; } - public IPackFileContainer FileOwner { get; } - public PackFile? Item { get; } - public TreeNodeSource? Parent { get; private set; } - public List Children { get; } = []; - public bool IsVisible { get; set; } = true; - public bool UnsavedChanged { get; set; } - public TreeNode? MaterializedNode { get; set; } - - /// - /// Returns true if this node has children (loaded or potentially unloaded). - /// - public bool HasChildren => Children.Count > 0 || (!_childrenLoaded && _childLoader != null && NodeType != NodeType.File); - - /// - /// Returns true if children have been loaded from the container. - /// - public bool ChildrenLoaded => _childrenLoaded; - - public TreeNodeSource(string name, NodeType nodeType, IPackFileContainer fileOwner, TreeNodeSource? parent, PackFile? item = null) - { - Name = name; - NodeType = nodeType; - FileOwner = fileOwner; - Parent = parent; - Item = item; - - if (string.IsNullOrWhiteSpace(name)) - throw new Exception($"Packfile name or folder is empty '{GetFullPath()}', this is not allowed! Please report as a bug if it happens outside of packfile loading! If it happens while loading clean up the packfile in RPFM"); - } - - /// - /// Sets the child loader delegate used for lazy population of children. - /// The delegate receives this node and returns true if children were loaded. - /// - public void SetChildLoader(Func childLoader) - { - _childLoader = childLoader; - } - - /// - /// Ensures children are populated. If not yet loaded and a child loader is set, loads them now. - /// - public void EnsureChildrenPopulated() - { - if (_childrenLoaded || _childLoader == null || NodeType == NodeType.File) - return; - - _childrenLoaded = _childLoader(this); - } - - /// - /// Recursively ensures all descendants are populated. - /// - public void EnsureFullyPopulated() - { - EnsureChildrenPopulated(); - foreach (var child in Children) - child.EnsureFullyPopulated(); - } - - /// - /// Marks children as already loaded (used when children are added externally). - /// - public void MarkChildrenLoaded() - { - _childrenLoaded = true; - } - - public string GetFullPath() - { - if (NodeType == NodeType.Root) - return string.Empty; - - var currentParent = Parent; - var path = Name; - while (currentParent != null) - { - if (currentParent.NodeType == NodeType.Root) - break; - - path = currentParent.Name + "\\" + path; - currentParent = currentParent.Parent; - } - - return path; - } - - public void AddChild(TreeNodeSource child) - { - child.Parent = this; - Children.Add(child); - } - - public void RemoveChild(TreeNodeSource child) - { - Children.Remove(child); - child.Parent = null; - } - - public IEnumerable EnumerateAllNodesDepthFirst() - { - var stack = new Stack(); - stack.Push(this); - - while (stack.Count > 0) - { - var current = stack.Pop(); - current.EnsureChildrenPopulated(); - yield return current; - - for (var i = current.Children.Count - 1; i >= 0; i--) - stack.Push(current.Children[i]); - } - } - - public IEnumerable EnumerateFileNodesDepthFirst() - { - foreach (var node in EnumerateAllNodesDepthFirst()) - { - if (node.NodeType == NodeType.File) - yield return node; - } - } - } -} \ No newline at end of file diff --git a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs index 6eab570f9..e442b1396 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs @@ -1,20 +1,21 @@ // PackFileBrowserViewModel Lazy Loading Architecture: // // The tree is built lazily. When a container is added (via PackFileContainerAddedEvent), only -// the root TreeNodeSource is created with a child-loader delegate. Children are NOT loaded from +// the root TreeNode is created with a child-loader delegate. Children are NOT loaded from // the container's GetDirectoryContent() until the node is expanded or explicitly queried. // -// TreeNodeSource is the canonical backing model (lightweight, no UI state). -// TreeNode is the materialized WPF-bound node, created on demand when a branch is expanded. -// A placeholder child is used so the WPF TreeView shows the expand arrow for unloaded directories. +// TreeNode serves as both the data model (BackingChildren) and the WPF-bound node. +// BackingChildren holds the full logical tree once loaded; Children (ObservableCollection) holds +// only the materialized subset visible to WPF. A placeholder child is used so the WPF TreeView +// shows the expand arrow for unloaded directories. // // Event-driven updates from PackFileService: -// - PackFileContainerAddedEvent → ReloadTree: creates root source + lazy child loader -// - PackFileContainerRemovedEvent → removes container's tree state and TreeNode from Files -// - PackFileContainerFilesAddedEvent → AddFiles: inserts nodes into loaded source branches, creates dirs as needed -// - PackFileContainerFilesRemovedEvent → removes source + materialized nodes for deleted files -// - PackFileContainerFilesUpdatedEvent → updates Name/UnsavedChanged on source nodes (for rename/save) -// - PackFileContainerFolderRemovedEvent → finds and removes folder source node and materialized subtree +// - PackFileContainerAddedEvent → ReloadTree: creates root node + lazy child loader +// - PackFileContainerRemovedEvent → removes container's root and TreeNode from Files +// - PackFileContainerFilesAddedEvent → AddFiles: inserts nodes into loaded branches, creates dirs as needed +// - PackFileContainerFilesRemovedEvent → removes nodes for deleted files +// - PackFileContainerFilesUpdatedEvent → updates Name/UnsavedChanged on nodes (for rename/save) +// - PackFileContainerFolderRemovedEvent → finds and removes folder node and subtree // - PackFileContainerFolderRenamedEvent → finds folder by OLD path, renames leaf, marks unsaved // - PackFileContainerSetAsMainEditableEvent → toggles IsMainEditabelPack on root TreeNodes // - PackFileContainerSavedEvent → clears UnsavedChanged on loaded nodes only (avoids forcing full population) @@ -632,6 +633,108 @@ public void ShowFoldersOnly_HidesFiles() Assert.That(folderA.Children.All(c => c.NodeType != NodeType.File || !c.IsVisible), Is.True, "No visible file nodes should exist under folder with ShowFoldersOnly"); } + + [Test] + public void SearchAutoExpandsWhenResultCountBelowThreshold() + { + // Create a small number of files (below default threshold of 25) + CreatePackfiles( + ("folderA\\sub\\file1.txt", "file1.txt"), + ("folderA\\sub\\file2.txt", "file2.txt"), + ("folderB\\file3.txt", "file3.txt")); + + var root = _viewModel.Files[0]; + Assert.That(root.IsNodeExpanded, Is.False, "Root should start collapsed"); + + // Filter matches all 3 files — well below the 25 threshold + _viewModel.Filter.FilterText = "file"; + + // All matching nodes should be auto-expanded + Assert.That(root.IsNodeExpanded, Is.True, "Root should be expanded when results < threshold"); + + var folderA = _viewModel.GetFromPath(root, "foldera"); + Assert.That(folderA, Is.Not.Null); + Assert.That(folderA.IsNodeExpanded, Is.True, "folderA should be auto-expanded"); + + var sub = _viewModel.GetFromPath(root, "foldera\\sub"); + Assert.That(sub, Is.Not.Null); + Assert.That(sub.IsNodeExpanded, Is.True, "sub should be auto-expanded"); + + var folderB = _viewModel.GetFromPath(root, "folderb"); + Assert.That(folderB, Is.Not.Null); + Assert.That(folderB.IsNodeExpanded, Is.True, "folderB should be auto-expanded"); + } + + [Test] + public void SearchDoesNotAutoExpandWhenResultCountAboveThreshold() + { + // Create more files than the threshold + var files = new List<(string Path, string FileName)>(); + for (var i = 0; i < 30; i++) + files.Add(($"folder{i}\\file{i}.txt", $"file{i}.txt")); + + CreatePackfiles(files.ToArray()); + + var root = _viewModel.Files[0]; + + // Set threshold low to ensure we exceed it + _viewModel.Filter.AutoExapandResultsAfterLimitedCount = 5; + _viewModel.Filter.FilterText = "file"; + + // Root gets expanded by ExpandForFilter (always happens), but children should NOT be recursively expanded + Assert.That(root.IsNodeExpanded, Is.True, "Root is always expanded by ExpandForFilter"); + + // Check that deep nodes are NOT expanded (since results exceed threshold) + var expandedFolders = root.Children.Where(c => c.IsNodeExpanded).ToList(); + Assert.That(expandedFolders.Count, Is.EqualTo(0), + "Folders should NOT be auto-expanded when results exceed threshold"); + } + + [Test] + public void DoubleClickCollapseAndReexpand_ChildrenRemainVisible() + { + // Scenario: user expands folders manually, collapses a parent, + // then re-expands it — child folders that were expanded should still show their children. + CreatePackfiles( + ("folderA\\sub\\file1.txt", "file1.txt"), + ("folderA\\sub\\file2.txt", "file2.txt")); + + var root = _viewModel.Files[0]; + + // Expand the tree manually + root.IsNodeExpanded = true; + var folderA = _viewModel.GetFromPath(root, "foldera"); + Assert.That(folderA, Is.Not.Null); + folderA.IsNodeExpanded = true; + var sub = _viewModel.GetFromPath(root, "foldera\\sub"); + Assert.That(sub, Is.Not.Null); + sub.IsNodeExpanded = true; + + // Verify files are visible + var file1 = _viewModel.GetFromPath(root, "foldera\\sub\\file1.txt"); + Assert.That(file1, Is.Not.Null); + + // User double-clicks root to collapse + _viewModel.DoubleClickCommand.Execute(root); + Assert.That(root.IsNodeExpanded, Is.False); + + // User double-clicks root to re-expand + _viewModel.DoubleClickCommand.Execute(root); + Assert.That(root.IsNodeExpanded, Is.True); + + // folderA should still be visible and expanded with its children materialized + folderA = _viewModel.GetFromPath(root, "foldera"); + Assert.That(folderA, Is.Not.Null, "folderA should be visible after re-expanding root"); + Assert.That(folderA.IsNodeExpanded, Is.True, "folderA should still be expanded"); + + sub = _viewModel.GetFromPath(root, "foldera\\sub"); + Assert.That(sub, Is.Not.Null, "sub should be visible since folderA is expanded"); + Assert.That(sub.IsNodeExpanded, Is.True, "sub should still be expanded"); + + // Files inside sub should be materialized + file1 = _viewModel.GetFromPath(root, "foldera\\sub\\file1.txt"); + Assert.That(file1, Is.Not.Null, "file1 should be visible inside expanded sub folder"); + } } } From ab0ea383bb02f9cde22e4c66a670f3a53244b00d Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Wed, 6 May 2026 22:06:09 +0200 Subject: [PATCH 02/26] code --- .../Containers/CachedPackFileContainer.cs | 116 ++++++++---------- 1 file changed, 51 insertions(+), 65 deletions(-) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs index dd0a8c6f7..c20f3fc6b 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs @@ -8,7 +8,7 @@ namespace Shared.Core.PackFiles.Models.Containers { internal class CachedPackFileContainer : IPackFileContainerInternal { - private readonly DbContextOptions _dbOptions; + private readonly CacheDbContext _db; public string Name { get; set; } public bool IsCaPackFile { get => true; set { } } @@ -19,42 +19,38 @@ public CachedPackFileContainer(string name, DbContextOptions dbO { Name = name; SystemFilePath = string.Empty; - _dbOptions = dbOptions; + _db = new CacheDbContext(dbOptions); + _db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } public int GetFileCount() { - using var db = new CacheDbContext(_dbOptions); - return db.Files.Count(); + return _db.Files.Count(); } public PackFile? FindFile(string path) { var lowerPath = PathNormalization.NormalizeFileName(path); - using var db = new CacheDbContext(_dbOptions); - var entry = db.Files.AsNoTracking().FirstOrDefault(f => f.RelativePath == lowerPath); + var entry = _db.Files.FirstOrDefault(f => f.RelativePath == lowerPath); return entry != null ? ToPackFile(entry) : null; } public bool ContainsFile(string path) { var lowerPath = PathNormalization.NormalizeFileName(path); - using var db = new CacheDbContext(_dbOptions); - return db.Files.Any(f => f.RelativePath == lowerPath); + return _db.Files.Any(f => f.RelativePath == lowerPath); } public string? GetFullPath(PackFile file) { - using var db = new CacheDbContext(_dbOptions); - var entry = db.Files.AsNoTracking().FirstOrDefault(f => f.FileName == file.Name); + var entry = _db.Files.FirstOrDefault(f => f.FileName == file.Name); return entry?.RelativePath; } public List<(string FileName, PackFile Pack)> FindAllWithExtention(string extention) { extention = extention.ToLower(); - using var db = new CacheDbContext(_dbOptions); - var entries = db.Files.AsNoTracking() + var entries = _db.Files .Where(f => f.Extension == extention) .ToList(); @@ -63,20 +59,16 @@ public bool ContainsFile(string path) public List<(string Path, PackFile File)> SearchFiles(string? textFilter, IReadOnlyList? extensions) { - using var db = new CacheDbContext(_dbOptions); - - IQueryable query = db.Files.AsNoTracking(); + IQueryable query = _db.Files; if (extensions != null && extensions.Count > 0) { - // Filter by extensions using SQL — handle most common case of actual file extensions var extList = extensions.Select(e => e.ToLowerInvariant()).ToList(); query = query.Where(f => extList.Any(ext => f.FileName.Contains(ext))); } if (!string.IsNullOrWhiteSpace(textFilter)) { - // Use LIKE for case-insensitive substring matching in SQLite var pattern = $"%{textFilter}%"; query = query.Where(f => EF.Functions.Like(f.FileName, pattern)); } @@ -88,8 +80,7 @@ public bool ContainsFile(string path) public Dictionary GetAllFiles() { - using var db = new CacheDbContext(_dbOptions); - var entries = db.Files.AsNoTracking().ToList(); + var entries = _db.Files.ToList(); var parentCache = new Dictionary(StringComparer.OrdinalIgnoreCase); var result = new Dictionary(entries.Count); @@ -117,64 +108,59 @@ public void AddOrUpdateFile(string path, PackFile file) => public DirectoryContent GetDirectoryContent(string directoryPath) { - using var db = new CacheDbContext(_dbOptions); + var prefix = string.IsNullOrEmpty(directoryPath) ? "" : directoryPath + "\\"; + var prefixLen = prefix.Length; - // Get files directly in this folder - var filesInDir = db.Files.AsNoTracking() - .Where(f => f.FolderPath == directoryPath) - .Select(f => new { f.FileName, f.SourcePackFilePath, f.Offset, f.Size, f.IsEncrypted, f.IsCompressed, f.CompressionFormat, f.UncompressedSize }) + // Single DB query: load all rows needed to derive both direct files and immediate subfolders. + // - root: all rows are relevant + // - non-root: direct files (FolderPath == directoryPath) and descendants (FolderPath.StartsWith(prefix)) + var allNeededRows = _db.Files + .Where(f => string.IsNullOrEmpty(directoryPath) + || f.FolderPath == directoryPath + || f.FolderPath.StartsWith(prefix)) + .Select(f => new { f.FolderPath, f.FileName, f.SourcePackFilePath, f.Offset, f.Size, f.IsEncrypted, f.IsCompressed, f.CompressionFormat, f.UncompressedSize }) .ToList(); - var files = filesInDir + var parentCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Create all files + var files = allNeededRows + .Where(f => f.FolderPath == directoryPath) .Select(f => { - var parent = new PackedFileSourceParent { FilePath = f.SourcePackFilePath }; + if (!parentCache.TryGetValue(f.SourcePackFilePath, out var parent)) + { + parent = new PackedFileSourceParent { FilePath = f.SourcePackFilePath }; + parentCache[f.SourcePackFilePath] = parent; + } + var source = new PackedFileSource(parent, f.Offset, f.Size, f.IsEncrypted, f.IsCompressed, (Utility.CompressionFormat)f.CompressionFormat, f.UncompressedSize); return (f.FileName, File: new PackFile(f.FileName, source)); }) .OrderBy(x => x.FileName, StringComparer.CurrentCultureIgnoreCase) .ToList(); - // Get immediate subfolders - var prefix = string.IsNullOrEmpty(directoryPath) ? "" : directoryPath + "\\"; - var prefixLen = prefix.Length; + // Works for both root and non-root directories: + // - root: candidate is full folder path (e.g. "models\\textures") => "models" + // - non-root: candidate is path remainder after prefix (e.g. "textures\\specular") => "textures" + var subFolders = allNeededRows + .Where(f => string.IsNullOrEmpty(directoryPath) + ? f.FolderPath != "" + : f.FolderPath.StartsWith(prefix)) + .Select(f => f.FolderPath) + .Select(folderPath => + { + var candidate = string.IsNullOrEmpty(directoryPath) + ? folderPath + : folderPath.Substring(prefixLen); - List subFolders; - if (string.IsNullOrEmpty(directoryPath)) - { - // Root level: get distinct first path segment from all FolderPaths - subFolders = db.Files.AsNoTracking() - .Where(f => f.FolderPath != "") - .Select(f => f.FolderPath) - .Distinct() - .AsEnumerable() - .Select(fp => - { - var sepIdx = fp.IndexOf('\\'); - return sepIdx == -1 ? fp : fp.Substring(0, sepIdx); - }) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase) - .ToList(); - } - else - { - // Non-root: get distinct next segment from FolderPaths that start with prefix - subFolders = db.Files.AsNoTracking() - .Where(f => f.FolderPath.StartsWith(prefix)) - .Select(f => f.FolderPath) - .Distinct() - .AsEnumerable() - .Select(fp => - { - var remainder = fp.Substring(prefixLen); - var sepIdx = remainder.IndexOf('\\'); - return sepIdx == -1 ? remainder : remainder.Substring(0, sepIdx); - }) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase) - .ToList(); - } + var sepIdx = candidate.IndexOf('\\'); + return sepIdx == -1 ? candidate : candidate.Substring(0, sepIdx); + }) + .Where(folderName => !string.IsNullOrWhiteSpace(folderName)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase) + .ToList(); return new DirectoryContent { From 55b00d6118817c02518dc1ed887ca19446fe16e9 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 7 May 2026 20:36:34 +0200 Subject: [PATCH 03/26] Code --- .../Containers/CachedPackFileContainer.cs | 69 +++---- .../Models/Containers/PackFileContainer.cs | 47 ++--- .../PackFiles/Models/IPackFileContainer.cs | 15 +- .../Utility/PackFileServiceUtility.cs | 24 +++ .../CachedPackFileContainerTests.cs | 179 ++++++++++++++---- .../Containers/PackFileContainerTests.cs | 61 ++++++ .../PackFileContainerCacheHelperTests.cs | 10 +- .../Utility/PackFileServiceUtilityTests.cs | 61 ++++++ .../PackFileTree/PackFileBrowserViewModel.cs | 9 +- .../BaseDialogs/PackFileTree/TreeNode.cs | 7 +- .../SharedUI/Shared.UiTest/TreeNodeTests.cs | 19 ++ 11 files changed, 385 insertions(+), 116 deletions(-) create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/PackFileServiceUtilityTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/TreeNodeTests.cs diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs index c20f3fc6b..a4a3d87a0 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs @@ -103,72 +103,63 @@ public Dictionary GetAllFiles() return result; } - public void AddOrUpdateFile(string path, PackFile file) => - throw new InvalidOperationException("Cannot modify a cached CA pack file container."); - public DirectoryContent GetDirectoryContent(string directoryPath) - { - var prefix = string.IsNullOrEmpty(directoryPath) ? "" : directoryPath + "\\"; - var prefixLen = prefix.Length; - // Single DB query: load all rows needed to derive both direct files and immediate subfolders. - // - root: all rows are relevant - // - non-root: direct files (FolderPath == directoryPath) and descendants (FolderPath.StartsWith(prefix)) - var allNeededRows = _db.Files - .Where(f => string.IsNullOrEmpty(directoryPath) - || f.FolderPath == directoryPath - || f.FolderPath.StartsWith(prefix)) + public List<(string Path, PackFile File)> GetDirectoryContent(string directoryPath) + { + var directFileRows = _db.Files + .Where(f => f.FolderPath == directoryPath) .Select(f => new { f.FolderPath, f.FileName, f.SourcePackFilePath, f.Offset, f.Size, f.IsEncrypted, f.IsCompressed, f.CompressionFormat, f.UncompressedSize }) .ToList(); - var parentCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // Create all files - var files = allNeededRows - .Where(f => f.FolderPath == directoryPath) + var packedFileSourceParentCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + var files = directFileRows .Select(f => { - if (!parentCache.TryGetValue(f.SourcePackFilePath, out var parent)) + if (!packedFileSourceParentCache.TryGetValue(f.SourcePackFilePath, out var parent)) { parent = new PackedFileSourceParent { FilePath = f.SourcePackFilePath }; - parentCache[f.SourcePackFilePath] = parent; + packedFileSourceParentCache[f.SourcePackFilePath] = parent; } var source = new PackedFileSource(parent, f.Offset, f.Size, f.IsEncrypted, f.IsCompressed, (Utility.CompressionFormat)f.CompressionFormat, f.UncompressedSize); - return (f.FileName, File: new PackFile(f.FileName, source)); + var relativePath = string.IsNullOrEmpty(f.FolderPath) ? f.FileName : $"{f.FolderPath}\\{f.FileName}"; + return (Path: relativePath, File: new PackFile(f.FileName, source)); }) - .OrderBy(x => x.FileName, StringComparer.CurrentCultureIgnoreCase) + .OrderBy(x => x.Path, StringComparer.CurrentCultureIgnoreCase) .ToList(); - // Works for both root and non-root directories: - // - root: candidate is full folder path (e.g. "models\\textures") => "models" - // - non-root: candidate is path remainder after prefix (e.g. "textures\\specular") => "textures" - var subFolders = allNeededRows + return files; + } + + public List GetSubDirectories(string directoryPath) + { + var prefix = string.IsNullOrEmpty(directoryPath) ? "" : directoryPath + "\\"; + var prefixLength = prefix.Length; + + return _db.Files .Where(f => string.IsNullOrEmpty(directoryPath) ? f.FolderPath != "" : f.FolderPath.StartsWith(prefix)) .Select(f => f.FolderPath) - .Select(folderPath => + .AsEnumerable() + .Select(folderPath => string.IsNullOrEmpty(directoryPath) + ? folderPath + : folderPath.Substring(prefixLength)) + .Select(candidate => { - var candidate = string.IsNullOrEmpty(directoryPath) - ? folderPath - : folderPath.Substring(prefixLen); - - var sepIdx = candidate.IndexOf('\\'); - return sepIdx == -1 ? candidate : candidate.Substring(0, sepIdx); + var separatorIndex = candidate.IndexOf('\\'); + return separatorIndex == -1 ? candidate : candidate.Substring(0, separatorIndex); }) .Where(folderName => !string.IsNullOrWhiteSpace(folderName)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase) .ToList(); - - return new DirectoryContent - { - SubFolders = subFolders, - Files = files - }; } + public void AddOrUpdateFile(string path, PackFile file) => + throw new InvalidOperationException("Cannot modify a cached CA pack file container."); + public List AddFiles(List newFiles) => throw new InvalidOperationException("Cannot modify a cached CA pack file container."); diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs index 87ad5328d..33176d98a 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs @@ -27,39 +27,40 @@ public void AddOrUpdateFile(string path, PackFile file) public Dictionary GetAllFiles() => FileList; - public DirectoryContent GetDirectoryContent(string directoryPath) + public List<(string Path, PackFile File)> GetDirectoryContent(string directoryPath) { var prefix = string.IsNullOrEmpty(directoryPath) ? "" : directoryPath + "\\"; - var prefixLength = prefix.Length; - var subFolders = new HashSet(StringComparer.OrdinalIgnoreCase); - var files = new List<(string FileName, PackFile File)>(); + var results = new List<(string Path, PackFile File)>(); + var directFileSlashCount = string.IsNullOrEmpty(directoryPath) ? 0 : directoryPath.Count(c => c == '\\') + 1; foreach (var (path, packFile) in FileList) { - if (prefixLength > 0 && !path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - continue; - if (prefixLength == 0 && path.Length == 0) + if ((string.IsNullOrEmpty(prefix) || path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + && path.Count(c => c == '\\') == directFileSlashCount) + results.Add((path, packFile)); + } + + results.Sort((a, b) => StringComparer.CurrentCultureIgnoreCase.Compare(a.Path, b.Path)); + return results; + } + + public List GetSubDirectories(string directoryPath) + { + var prefix = string.IsNullOrEmpty(directoryPath) ? "" : directoryPath + "\\"; + var subFolders = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var path in FileList.Keys) + { + if (!string.IsNullOrEmpty(prefix) && !path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue; - var remainder = path.AsSpan(prefixLength); + var remainder = string.IsNullOrEmpty(prefix) ? path : path.Substring(prefix.Length); var separatorIndex = remainder.IndexOf(Path.DirectorySeparatorChar); - - if (separatorIndex == -1) - { - files.Add((packFile.Name, packFile)); - } - else - { - var folderName = remainder.Slice(0, separatorIndex).ToString(); - subFolders.Add(folderName); - } + if (separatorIndex > 0) + subFolders.Add(remainder.Substring(0, separatorIndex)); } - return new DirectoryContent - { - SubFolders = subFolders.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList(), - Files = files.OrderBy(x => x.FileName, StringComparer.CurrentCultureIgnoreCase).ToList() - }; + return subFolders.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList(); } public List<(string FileName, PackFile Pack)> FindAllWithExtention(string extention) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs index ca34efe30..007d76ff1 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs @@ -2,18 +2,6 @@ namespace Shared.Core.PackFiles.Models { - public readonly struct DirectoryContent - { - public IReadOnlyList SubFolders { get; init; } - public IReadOnlyList<(string FileName, PackFile File)> Files { get; init; } - - public static DirectoryContent Empty => new() - { - SubFolders = [], - Files = [] - }; - } - public interface IPackFileContainer { string Name { get; } @@ -28,7 +16,8 @@ public interface IPackFileContainer void AddOrUpdateFile(string path, PackFile file); List<(string FileName, PackFile Pack)> FindAllWithExtention(string extention); Dictionary GetAllFiles(); - DirectoryContent GetDirectoryContent(string directoryPath); + List<(string Path, PackFile File)> GetDirectoryContent(string directoryPath); + List GetSubDirectories(string directoryPath); List<(string Path, PackFile File)> SearchFiles(string? textFilter, IReadOnlyList? extensions); } } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs index c27799653..3c48c1f09 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs @@ -2,8 +2,32 @@ namespace Shared.Core.PackFiles.Utility { + public sealed record DirectoryFileEntry(string FileName, PackFile File); + + public sealed record DirectoryEntriesSplitResult( + string DirectoryPath, + IReadOnlyList SubFolders, + IReadOnlyList Files); + public static class PackFileServiceUtility { + public static DirectoryEntriesSplitResult SplitDirectoryEntries(IPackFileContainer container, string directoryPath) + { + var files = container.GetDirectoryContent(directoryPath) + .Select(entry => new DirectoryFileEntry(entry.File.Name, entry.File)) + .OrderBy(x => x.FileName, StringComparer.CurrentCultureIgnoreCase) + .ToList(); + + var subFolders = container.GetSubDirectories(directoryPath) + .OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase) + .ToList(); + + return new DirectoryEntriesSplitResult( + directoryPath, + subFolders, + files); + } + public static List GetAllAnimPacks(IPackFileService pfs) { var animPacks = FindAllWithExtention(pfs, @".animpack"); diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs index 856d76fe4..1f85e264c 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs @@ -280,7 +280,7 @@ public void TearDown() [Test] public void Root_ReturnsRootFilesAndTopFolders() { - var content = _container.GetDirectoryContent(""); + var content = PackFileServiceUtility.SplitDirectoryEntries(_container, ""); Assert.That(content.Files.Any(f => f.FileName == "root_file.txt"), Is.True); Assert.That(content.SubFolders, Does.Contain("models")); @@ -292,77 +292,80 @@ public void Root_ReturnsRootFilesAndTopFolders() [Test] public void Root_DoesNotIncludeFilesFromSubfolders() { - var content = _container.GetDirectoryContent(""); + var entries = _container.GetDirectoryContent(""); - Assert.That(content.Files.Count, Is.EqualTo(1)); - Assert.That(content.Files[0].FileName, Is.EqualTo("root_file.txt")); + Assert.That(entries.Count, Is.EqualTo(1)); + Assert.That(entries[0].File.Name, Is.EqualTo("root_file.txt")); } [Test] public void Subfolder_ReturnsDirectFilesAndImmediateSubfolders() { - var content = _container.GetDirectoryContent("models"); + var entries = _container.GetDirectoryContent("models"); + var subFolders = _container.GetSubDirectories("models"); - Assert.That(content.Files.Any(f => f.FileName == "unit.model"), Is.True); - Assert.That(content.Files.Any(f => f.FileName == "vehicle.model"), Is.True); - Assert.That(content.SubFolders, Does.Contain("textures")); - // Should NOT include files from deeper levels - Assert.That(content.Files.Any(f => f.FileName == "diffuse.dds"), Is.False); + Assert.That(entries.Any(f => f.File.Name == "unit.model"), Is.True); + Assert.That(entries.Any(f => f.File.Name == "vehicle.model"), Is.True); + Assert.That(subFolders, Does.Contain("textures")); + Assert.That(entries.Any(f => f.File.Name == "diffuse.dds"), Is.False); } [Test] public void NestedSubfolder_ReturnsCorrectContent() { - var content = _container.GetDirectoryContent("models\\textures"); + var entries = _container.GetDirectoryContent("models\\textures"); + var subFolders = _container.GetSubDirectories("models\\textures"); - Assert.That(content.Files.Any(f => f.FileName == "diffuse.dds"), Is.True); - Assert.That(content.Files.Any(f => f.FileName == "normal.dds"), Is.True); - Assert.That(content.SubFolders, Does.Contain("specular")); - Assert.That(content.Files.Count, Is.EqualTo(2)); + Assert.That(entries.Any(f => f.File.Name == "diffuse.dds"), Is.True); + Assert.That(entries.Any(f => f.File.Name == "normal.dds"), Is.True); + Assert.That(subFolders, Does.Contain("specular")); + Assert.That(entries.Count, Is.EqualTo(2)); } [Test] public void DeepNestedFolder_ReturnsOnlyItsFiles() { - var content = _container.GetDirectoryContent("models\\textures\\specular"); + var entries = _container.GetDirectoryContent("models\\textures\\specular"); + var subFolders = _container.GetSubDirectories("models\\textures\\specular"); - Assert.That(content.Files.Count, Is.EqualTo(1)); - Assert.That(content.Files[0].FileName, Is.EqualTo("gloss.dds")); - Assert.That(content.SubFolders, Is.Empty); + Assert.That(entries.Count, Is.EqualTo(1)); + Assert.That(entries[0].File.Name, Is.EqualTo("gloss.dds")); + Assert.That(subFolders, Is.Empty); } [Test] public void NonexistentFolder_ReturnsEmpty() { - var content = _container.GetDirectoryContent("nonexistent\\path"); + var entries = _container.GetDirectoryContent("nonexistent\\path"); + var subFolders = _container.GetSubDirectories("nonexistent\\path"); - Assert.That(content.Files, Is.Empty); - Assert.That(content.SubFolders, Is.Empty); + Assert.That(entries, Is.Empty); + Assert.That(subFolders, Is.Empty); } [Test] public void SubfoldersAreSorted() { - var content = _container.GetDirectoryContent(""); + var subFolders = _container.GetSubDirectories(""); - var sorted = content.SubFolders.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList(); - Assert.That(content.SubFolders, Is.EqualTo(sorted)); + var sorted = subFolders.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList(); + Assert.That(subFolders, Is.EqualTo(sorted)); } [Test] public void FilesAreSorted() { - var content = _container.GetDirectoryContent("models"); + var entries = _container.GetDirectoryContent("models"); - var sorted = content.Files.OrderBy(x => x.FileName, StringComparer.CurrentCultureIgnoreCase).ToList(); - Assert.That(content.Files.Select(f => f.FileName), Is.EqualTo(sorted.Select(f => f.FileName))); + var sorted = entries.OrderBy(x => x.File.Name, StringComparer.CurrentCultureIgnoreCase).ToList(); + Assert.That(entries.Select(f => f.File.Name), Is.EqualTo(sorted.Select(f => f.File.Name))); } [Test] public void Files_HaveCorrectDataSource() { - var content = _container.GetDirectoryContent("models"); - var unitFile = content.Files.First(f => f.FileName == "unit.model"); + var entries = _container.GetDirectoryContent("models"); + var unitFile = entries.First(f => f.File.Name == "unit.model"); var source = unitFile.File.DataSource as PackedFileSource; Assert.That(source, Is.Not.Null); @@ -482,9 +485,8 @@ public void GetDirectoryContent_EmptyContainer_ReturnsEmpty() PackFileContainerCacheHelper.SaveCache("fp", sourceContainer, dbOptions); var container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp")!; - var content = container.GetDirectoryContent(""); - Assert.That(content.Files, Is.Empty); - Assert.That(content.SubFolders, Is.Empty); + Assert.That(container.GetDirectoryContent(""), Is.Empty); + Assert.That(container.GetSubDirectories(""), Is.Empty); } } @@ -635,4 +637,115 @@ public void EmptyExtensionList_TreatedAsNoFilter() Assert.That(results.Count, Is.EqualTo(7)); } } + + [TestFixture] + public class CachedPackFileContainer_GetDirectoryContent_PathRows + { + private string _tempDir = null!; + private string _dbPath = null!; + private CachedPackFileContainer _container = null!; + + // Test structure (backslash delimited, invariant: every folder has at least one direct file): + // audio\filea + // audio\fileb + // texture\textureFile <- direct file so "texture" sub-folder is discoverable + // texture\mesha\filea + // texture\meshb\filea + // rootfilea + // rootfileb + + [SetUp] + public void SetUp() + { + _tempDir = Path.Combine(Path.GetTempPath(), "QueryDirTests_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + _dbPath = Path.Combine(_tempDir, "test_cache.db"); + + var sourceContainer = new PackFileContainer("TestCache") { IsCaPackFile = true, SystemFilePath = @"c:\game\data" }; + sourceContainer.SourcePackFilePaths.Add(@"c:\game\data\p.pack"); + var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\p.pack" }; + + sourceContainer.AddOrUpdateFile(@"audio\filea", new PackFile("filea", new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile(@"audio\fileb", new PackFile("fileb", new PackedFileSource(parent, 10, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile(@"texture\textureFile", new PackFile("textureFile", new PackedFileSource(parent, 20, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile(@"texture\mesha\filea", new PackFile("filea", new PackedFileSource(parent, 30, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile(@"texture\meshb\filea", new PackFile("filea", new PackedFileSource(parent, 40, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile(@"rootfilea", new PackFile("rootfilea", new PackedFileSource(parent, 50, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile(@"rootfileb", new PackFile("rootfileb", new PackedFileSource(parent, 60, 10, false, false, CompressionFormat.None, 0))); + + var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbPath); + PackFileContainerCacheHelper.SaveCache("test_fp", sourceContainer, dbOptions); + _container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "test_fp")!; + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_tempDir)) + Directory.Delete(_tempDir, true); + } + + [Test] + public void Root_IncludesOnlyRootFiles() + { + var rows = _container.GetDirectoryContent(""); + var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.That(paths, Does.Contain("rootfilea")); + Assert.That(paths, Does.Contain("rootfileb")); + } + + [Test] + public void Root_ExcludesDeepDescendantFolders() + { + var rows = _container.GetDirectoryContent(""); + var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.That(paths, Does.Not.Contain(@"texture\mesha\filea")); + Assert.That(paths, Does.Not.Contain(@"texture\meshb\filea")); + } + + [Test] + public void Root_ReturnsExpectedRowCount() + { + // root contains only its direct files + var rows = _container.GetDirectoryContent(""); + Assert.That(rows.Count, Is.EqualTo(2)); + } + + [Test] + public void Subfolder_IncludesOnlyDirectFiles() + { + var rows = _container.GetDirectoryContent(@"texture"); + var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.That(paths, Does.Contain(@"texture\textureFile")); + Assert.That(paths.Count, Is.EqualTo(1)); + } + + [Test] + public void Subfolder_ExcludesParentAndSiblingRows() + { + var rows = _container.GetDirectoryContent(@"texture"); + var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.That(paths.Any(p => p.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.False); + Assert.That(paths.Any(p => !p.StartsWith(@"texture\", StringComparison.OrdinalIgnoreCase)), Is.False); + } + + [Test] + public void Subfolder_ReturnsExpectedRowCount() + { + var rows = _container.GetDirectoryContent(@"texture"); + Assert.That(rows.Count, Is.EqualTo(1)); + } + + [Test] + public void LeafFolder_ReturnsOnlyDirectFiles() + { + var rows = _container.GetDirectoryContent(@"audio"); + Assert.That(rows.Count, Is.EqualTo(2)); + Assert.That(rows.All(r => r.Path.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.True); + } + } } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs index 3904f7320..d7047a663 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs @@ -2,6 +2,7 @@ using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.Containers; using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.PackFiles.Utility; namespace Shared.CoreTest.PackFiles.Models.Containers { @@ -616,4 +617,64 @@ public void EmptyExtensionList_TreatedAsNoFilter() Assert.That(results.Count, Is.EqualTo(7)); } } + + internal class PackFileContainer_GetDirectoryContent + { + [Test] + public void Root_ReturnsOnlyDirectFiles_Sorted() + { + var container = CreateContainer(); + + var entries = container.GetDirectoryContent(""); + var paths = entries.Select(x => x.Path).ToList(); + + Assert.That(paths, Is.EqualTo(new[] { "root_a.txt", "root_b.txt" })); + } + + [Test] + public void SubFolder_ReturnsOnlyDirectFiles() + { + var container = CreateContainer(); + + var entries = container.GetDirectoryContent("texture"); + + Assert.That(entries.Select(x => x.Path), Is.EqualTo(new[] { "texture\\texture_file.dds" })); + } + + [Test] + public void GetSubDirectories_ReturnsImmediateSubfolders() + { + var container = CreateContainer(); + + var subDirectories = container.GetSubDirectories("texture"); + + Assert.That(subDirectories, Is.EqualTo(new[] { "mesha", "meshb" })); + } + + [Test] + public void Utility_ComposesFilesAndSubfolders() + { + var container = CreateContainer(); + var split = PackFileServiceUtility.SplitDirectoryEntries(container, "texture"); + + Assert.That(split.Files.Select(x => x.FileName), Is.EqualTo(new[] { "texture_file.dds" })); + Assert.That(split.SubFolders, Is.EqualTo(new[] { "mesha", "meshb" })); + } + + private static PackFileContainer CreateContainer() + { + var container = new PackFileContainer("Test"); + var parent = new PackedFileSourceParent { FilePath = @"c:\game\p.pack" }; + + container.AddOrUpdateFile(@"audio\b.wem", new PackFile("b.wem", new PackedFileSource(parent, 0, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"audio\a.wem", new PackFile("a.wem", new PackedFileSource(parent, 1, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"texture\texture_file.dds", new PackFile("texture_file.dds", new PackedFileSource(parent, 2, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"texture\mesha\filea", new PackFile("filea", new PackedFileSource(parent, 3, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"texture\meshb\filea", new PackFile("filea", new PackedFileSource(parent, 4, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"root_b.txt", new PackFile("root_b.txt", new PackedFileSource(parent, 5, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"root_a.txt", new PackFile("root_a.txt", new PackedFileSource(parent, 6, 1, false, false, CompressionFormat.None, 0))); + + return container; + } + } } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs index 0b1023af0..83c0a9948 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs @@ -347,10 +347,12 @@ public void SaveCache_StoresFolderPathCorrectly() }; var parent = new PackedFileSourceParent { FilePath = @"c:\game\pack.pack" }; - container.AddOrUpdateFile("a\\b\\c\\file.txt", new PackFile("file.txt", + container.AddOrUpdateFile("a\\folder_marker.txt", new PackFile("folder_marker.txt", new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile("a\\b\\c\\file.txt", new PackFile("file.txt", + new PackedFileSource(parent, 10, 10, false, false, CompressionFormat.None, 0))); container.AddOrUpdateFile("root_file.txt", new PackFile("root_file.txt", - new PackedFileSource(parent, 10, 20, false, false, CompressionFormat.None, 0))); + new PackedFileSource(parent, 20, 20, false, false, CompressionFormat.None, 0))); var dbOptions = CreateTestDbOptions(); PackFileContainerCacheHelper.SaveCache("fp", container, dbOptions); @@ -359,11 +361,11 @@ public void SaveCache_StoresFolderPathCorrectly() var loaded = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp"); Assert.That(loaded, Is.Not.Null); - var rootContent = loaded.GetDirectoryContent(""); + var rootContent = PackFileServiceUtility.SplitDirectoryEntries(loaded, ""); Assert.That(rootContent.Files.Any(f => f.FileName == "root_file.txt"), Is.True); Assert.That(rootContent.SubFolders, Does.Contain("a")); - var deepContent = loaded.GetDirectoryContent("a\\b\\c"); + var deepContent = PackFileServiceUtility.SplitDirectoryEntries(loaded, "a\\b\\c"); Assert.That(deepContent.Files.Any(f => f.FileName == "file.txt"), Is.True); } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/PackFileServiceUtilityTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/PackFileServiceUtilityTests.cs new file mode 100644 index 000000000..2de8df6a0 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/PackFileServiceUtilityTests.cs @@ -0,0 +1,61 @@ +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.PackFiles.Utility; +using Shared.Core.Settings; + +namespace Shared.CoreTest.PackFiles.Utility +{ + internal class PackFileServiceUtilityTests + { + [Test] + public void SplitDirectoryEntries_ComposesSortedFoldersAndFiles() + { + var container = CreateContainer(); + + var result = PackFileServiceUtility.SplitDirectoryEntries(container, "texture"); + + Assert.That(result.DirectoryPath, Is.EqualTo("texture")); + Assert.That(result.SubFolders, Is.EqualTo(new[] { "mesha", "meshb" })); + Assert.That(result.Files.Select(x => x.FileName), Is.EqualTo(new[] { "alpha.dds", "texture_file.dds" })); + } + + [Test] + public void SplitDirectoryEntries_RootDirectory_ReturnsTopLevelFoldersAndFiles() + { + var container = CreateContainer(); + + var result = PackFileServiceUtility.SplitDirectoryEntries(container, ""); + + Assert.That(result.DirectoryPath, Is.EqualTo(string.Empty)); + Assert.That(result.SubFolders, Is.EqualTo(new[] { "audio", "texture" })); + Assert.That(result.Files.Select(x => x.FileName), Is.EqualTo(new[] { "root.txt" })); + } + + [Test] + public void SplitDirectoryEntries_EmptyDirectory_ReturnsEmptyResult() + { + var container = CreateContainer(); + var result = PackFileServiceUtility.SplitDirectoryEntries(container, "missing"); + + Assert.That(result.DirectoryPath, Is.EqualTo("missing")); + Assert.That(result.SubFolders, Is.Empty); + Assert.That(result.Files, Is.Empty); + } + + private static PackFileContainer CreateContainer() + { + var container = new PackFileContainer("Test"); + var parent = new PackedFileSourceParent { FilePath = @"c:\game\p.pack" }; + + container.AddOrUpdateFile(@"root.txt", new PackFile("root.txt", new PackedFileSource(parent, 0, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"audio\a.wem", new PackFile("a.wem", new PackedFileSource(parent, 1, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"texture\texture_file.dds", new PackFile("texture_file.dds", new PackedFileSource(parent, 2, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"texture\alpha.dds", new PackFile("alpha.dds", new PackedFileSource(parent, 3, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"texture\mesha\filea", new PackFile("filea", new PackedFileSource(parent, 4, 1, false, false, CompressionFormat.None, 0))); + container.AddOrUpdateFile(@"texture\meshb\fileb", new PackFile("fileb", new PackedFileSource(parent, 5, 1, false, false, CompressionFormat.None, 0))); + + return container; + } + } +} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs index b56fa09a9..3e77d95ad 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs @@ -10,6 +10,7 @@ using Shared.Core.Events.Global; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Utility; using Shared.Core.Settings; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu; using Shared.Ui.Common; @@ -420,17 +421,19 @@ private void ReloadTree(IPackFileContainer container) private bool LoadChildrenFromContainer(TreeNode node, IPackFileContainer container, bool skipWemFiles) { var directoryPath = node.GetFullPath(); - var content = container.GetDirectoryContent(directoryPath); + var split = PackFileServiceUtility.SplitDirectoryEntries(container, directoryPath); - foreach (var folderName in content.SubFolders) + foreach (var folderName in split.SubFolders) { var childNode = new TreeNode(folderName, NodeType.Directory, container, node, () => Filter.HasActiveFilter); childNode.SetChildLoader(n => LoadChildrenFromContainer(n, container, skipWemFiles)); node.AddChild(childNode); } - foreach (var (fileName, file) in content.Files) + foreach (var fileEntry in split.Files) { + var fileName = fileEntry.FileName; + var file = fileEntry.File; if (skipWemFiles && fileName.EndsWith(".wem", StringComparison.OrdinalIgnoreCase)) continue; diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs index d946527ba..4182bdc8f 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs @@ -29,7 +29,9 @@ public partial class TreeNode : ObservableObject public TreeNode? Parent { get; set; } public List BackingChildren { get; } = []; - public bool HasChildren => BackingChildren.Count > 0 || (!_childrenLoaded && _childLoader != null && NodeType != NodeType.File); + public bool HasChildren => NodeType == NodeType.Directory + ? (BackingChildren.Count > 0 || !_childrenLoaded) + : (BackingChildren.Count > 0 || (!_childrenLoaded && _childLoader != null && NodeType != NodeType.File)); public bool ChildrenLoaded => _childrenLoaded; [ObservableProperty] public partial ObservableCollection Children { get; set; } = []; @@ -52,6 +54,9 @@ public TreeNode(string name, NodeType type, IPackFileContainer owner, TreeNode? if (string.IsNullOrWhiteSpace(name)) throw new Exception($"Packfile name or folder is empty '{GetFullPath()}', this is not allowed! Please report as a bug if it happens outside of packfile loading! If it happens while loading clean up the packfile in RPFM"); + + if (ShouldUsePlaceholder()) + AddPlaceholderChild(); } internal TreeNode(string name, NodeType type, IPackFileContainer owner, TreeNode? parent, Func isFilterActive, PackFile? packFile = null) diff --git a/Shared/SharedUI/Shared.UiTest/TreeNodeTests.cs b/Shared/SharedUI/Shared.UiTest/TreeNodeTests.cs new file mode 100644 index 000000000..c577da06d --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/TreeNodeTests.cs @@ -0,0 +1,19 @@ +using Shared.Core.PackFiles.Models.Containers; +using Shared.Ui.BaseDialogs.PackFileTree; + +namespace Shared.UiTest +{ + internal class TreeNodeTests + { + [Test] + public void DirectoryWithoutLoadedChildren_ShowsPlaceholderForExpandIcon() + { + var container = new PackFileContainer("test.pack"); + + var directoryNode = new TreeNode("documents", NodeType.Directory, container, null); + + Assert.That(directoryNode.Children.Any(c => c.Name == ""), Is.True, + "Collapsed directory without loaded children should contain a placeholder so TreeView shows expand icon."); + } + } +} From 350662858879d7b0f688ec5a9d3f8733fc7c1530 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 7 May 2026 20:47:46 +0200 Subject: [PATCH 04/26] code --- .../CachedPackFileContainerTests.cs | 324 ++++-------------- 1 file changed, 72 insertions(+), 252 deletions(-) diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs index 1f85e264c..2404e3358 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs @@ -9,11 +9,11 @@ namespace Shared.CoreTest.PackFiles.Models.Containers { - internal class CachedPackFileContainer_ReadOnly + internal abstract class CachedPackFileContainer_TestBase { - private string _tempDir; - private string _dbFilePath; - private CachedPackFileContainer _container; + protected string _tempDir = null!; + protected string _dbFilePath = null!; + protected CachedPackFileContainer _container = null!; [SetUp] public void Setup() @@ -22,7 +22,7 @@ public void Setup() Directory.CreateDirectory(_tempDir); _dbFilePath = Path.Combine(_tempDir, "test.db"); - // Build a container with test data and save to DB + // Master test dataset used by all fixtures in this file. var sourceContainer = new PackFileContainer("TestCache") { IsCaPackFile = true, @@ -37,11 +37,33 @@ public void Setup() new PackedFileSource(parent, 300, 400, false, true, CompressionFormat.Lz4, 800))); sourceContainer.AddOrUpdateFile("audio\\sound.wem", new PackFile("sound.wem", new PackedFileSource(parent, 700, 500, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("root_file.txt", new PackFile("root_file.txt", + new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\unit.model", new PackFile("unit.model", + new PackedFileSource(parent, 10, 20, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\vehicle.model", new PackFile("vehicle.model", + new PackedFileSource(parent, 30, 40, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\textures\\diffuse.dds", new PackFile("diffuse.dds", + new PackedFileSource(parent, 70, 50, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\textures\\normal.dds", new PackFile("normal.dds", + new PackedFileSource(parent, 120, 60, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\textures\\specular\\gloss.dds", new PackFile("gloss.dds", + new PackedFileSource(parent, 180, 30, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("audio\\music.wem", new PackFile("music.wem", + new PackedFileSource(parent, 210, 100, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("audio\\battle_sound.wem", new PackFile("battle_sound.wem", + new PackedFileSource(parent, 400, 300, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("scripts\\campaign_script.lua", new PackFile("campaign_script.lua", + new PackedFileSource(parent, 850, 80, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("folder_a\\shared.txt", new PackFile("shared.txt", + new PackedFileSource(parent, 900, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("folder_b\\shared.txt", new PackFile("shared.txt", + new PackedFileSource(parent, 910, 20, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("compressed\\data.bin", new PackFile("data.bin", + new PackedFileSource(parent, 1000, 500, true, true, CompressionFormat.Lz4, 2000))); var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); PackFileContainerCacheHelper.SaveCache("test_fp", sourceContainer, dbOptions); - - // Load the cached container (lazy, no files in memory) _container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "test_fp")!; } @@ -51,6 +73,10 @@ public void TearDown() if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } + } + + internal class CachedPackFileContainer_ReadOnly : CachedPackFileContainer_TestBase + { [Test] public void IsCaPackFile_AlwaysTrue() @@ -68,7 +94,7 @@ public void IsCaPackFile_SetterDoesNotChangeValue() [Test] public void GetFileCount_ReturnsCorrectCount() { - Assert.That(_container.GetFileCount(), Is.EqualTo(3)); + Assert.That(_container.GetFileCount(), Is.EqualTo(15)); } [Test] @@ -135,7 +161,7 @@ public void GetFullPath_ReturnsNullForMissing() public void FindAllWithExtention_ReturnsMatching() { var results = _container.FindAllWithExtention(".wem"); - Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results.Count, Is.EqualTo(3)); Assert.That(results[0].FileName, Does.EndWith("sound.wem")); } @@ -150,7 +176,7 @@ public void FindAllWithExtention_ReturnsEmptyForNoMatch() public void GetAllFiles_ReturnsAllFiles() { var all = _container.GetAllFiles(); - Assert.That(all.Count, Is.EqualTo(3)); + Assert.That(all.Count, Is.EqualTo(15)); Assert.That(all.ContainsKey("folder\\file.txt"), Is.True); Assert.That(all.ContainsKey("other\\data.bin"), Is.True); Assert.That(all.ContainsKey("audio\\sound.wem"), Is.True); @@ -221,62 +247,8 @@ public void AddOrUpdateFile_Throws() } } - internal class CachedPackFileContainer_GetDirectoryContent + internal class CachedPackFileContainer_GetDirectoryContent : CachedPackFileContainer_TestBase { - private string _tempDir; - private string _dbFilePath; - private CachedPackFileContainer _container; - - [SetUp] - public void Setup() - { - _tempDir = Path.Combine(Path.GetTempPath(), "CachedDirContentTests_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempDir); - _dbFilePath = Path.Combine(_tempDir, "test.db"); - - var sourceContainer = new PackFileContainer("DirTest") - { - IsCaPackFile = true, - SystemFilePath = @"c:\game\data" - }; - sourceContainer.SourcePackFilePaths.Add(@"c:\game\data\pack1.pack"); - - var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\pack1.pack" }; - - // Root-level files - sourceContainer.AddOrUpdateFile("root_file.txt", new PackFile("root_file.txt", - new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); - - // Single-level folders - sourceContainer.AddOrUpdateFile("models\\unit.model", new PackFile("unit.model", - new PackedFileSource(parent, 10, 20, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\vehicle.model", new PackFile("vehicle.model", - new PackedFileSource(parent, 30, 40, false, false, CompressionFormat.None, 0))); - - // Nested folders - sourceContainer.AddOrUpdateFile("models\\textures\\diffuse.dds", new PackFile("diffuse.dds", - new PackedFileSource(parent, 70, 50, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\textures\\normal.dds", new PackFile("normal.dds", - new PackedFileSource(parent, 120, 60, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\textures\\specular\\gloss.dds", new PackFile("gloss.dds", - new PackedFileSource(parent, 180, 30, false, false, CompressionFormat.None, 0))); - - // Another top-level folder - sourceContainer.AddOrUpdateFile("audio\\music.wem", new PackFile("music.wem", - new PackedFileSource(parent, 210, 100, false, false, CompressionFormat.None, 0))); - - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); - PackFileContainerCacheHelper.SaveCache("fp", sourceContainer, dbOptions); - _container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp")!; - } - - [TearDown] - public void TearDown() - { - if (Directory.Exists(_tempDir)) - Directory.Delete(_tempDir, true); - } - [Test] public void Root_ReturnsRootFilesAndTopFolders() { @@ -375,101 +347,45 @@ public void Files_HaveCorrectDataSource() } } - internal class CachedPackFileContainer_EdgeCases + internal class CachedPackFileContainer_EdgeCases : CachedPackFileContainer_TestBase { - private string _tempDir; - private string _dbFilePath; - - [SetUp] - public void Setup() - { - _tempDir = Path.Combine(Path.GetTempPath(), "CachedEdgeCaseTests_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempDir); - _dbFilePath = Path.Combine(_tempDir, "test.db"); - } - - [TearDown] - public void TearDown() - { - if (Directory.Exists(_tempDir)) - Directory.Delete(_tempDir, true); - } - [Test] public void FindAllWithExtention_IsCaseInsensitive() { - var sourceContainer = new PackFileContainer("Test") { SystemFilePath = @"c:\game" }; - var parent = new PackedFileSourceParent { FilePath = @"c:\game\pack.pack" }; - sourceContainer.AddOrUpdateFile("sound.wem", new PackFile("sound.wem", - new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); - - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); - PackFileContainerCacheHelper.SaveCache("fp", sourceContainer, dbOptions); - var container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp")!; - - var upper = container.FindAllWithExtention(".WEM"); - var lower = container.FindAllWithExtention(".wem"); - Assert.That(upper.Count, Is.EqualTo(1)); - Assert.That(lower.Count, Is.EqualTo(1)); + var upper = _container.FindAllWithExtention(".WEM"); + var lower = _container.FindAllWithExtention(".wem"); + Assert.That(upper.Count, Is.EqualTo(3)); + Assert.That(lower.Count, Is.EqualTo(3)); } [Test] public void ContainsFile_NormalizesForwardSlashes() { - var sourceContainer = new PackFileContainer("Test") { SystemFilePath = @"c:\game" }; - var parent = new PackedFileSourceParent { FilePath = @"c:\game\pack.pack" }; - sourceContainer.AddOrUpdateFile("folder\\file.txt", new PackFile("file.txt", - new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); - - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); - PackFileContainerCacheHelper.SaveCache("fp", sourceContainer, dbOptions); - var container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp")!; - - Assert.That(container.ContainsFile("folder/file.txt"), Is.True); - Assert.That(container.ContainsFile("FOLDER/FILE.TXT"), Is.True); - Assert.That(container.ContainsFile(" folder\\file.txt "), Is.True); + Assert.That(_container.ContainsFile("folder/file.txt"), Is.True); + Assert.That(_container.ContainsFile("FOLDER/FILE.TXT"), Is.True); + Assert.That(_container.ContainsFile(" folder\\file.txt "), Is.True); } [Test] public void GetAllFiles_PreservesCompressionMetadata() { - var sourceContainer = new PackFileContainer("Test") { SystemFilePath = @"c:\game" }; - var parent = new PackedFileSourceParent { FilePath = @"c:\game\pack.pack" }; - sourceContainer.AddOrUpdateFile("data.bin", new PackFile("data.bin", - new PackedFileSource(parent, 100, 500, true, true, CompressionFormat.Lz4, 2000))); - - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); - PackFileContainerCacheHelper.SaveCache("fp", sourceContainer, dbOptions); - var container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp")!; - - var all = container.GetAllFiles(); - var source = (PackedFileSource)all["data.bin"].DataSource; + var all = _container.GetAllFiles(); + var source = (PackedFileSource)all["compressed\\data.bin"].DataSource; Assert.That(source.IsEncrypted, Is.True); Assert.That(source.IsCompressed, Is.True); Assert.That(source.CompressionFormat, Is.EqualTo(CompressionFormat.Lz4)); Assert.That(source.UncompressedSize, Is.EqualTo(2000)); - Assert.That(source.Offset, Is.EqualTo(100)); + Assert.That(source.Offset, Is.EqualTo(1000)); Assert.That(source.Size, Is.EqualTo(500)); } [Test] public void GetFullPath_WithDuplicateFileNames_ReturnsFirstMatch() { - var sourceContainer = new PackFileContainer("Test") { SystemFilePath = @"c:\game" }; - var parent = new PackedFileSourceParent { FilePath = @"c:\game\pack.pack" }; - sourceContainer.AddOrUpdateFile("folder_a\\shared.txt", new PackFile("shared.txt", - new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("folder_b\\shared.txt", new PackFile("shared.txt", - new PackedFileSource(parent, 10, 20, false, false, CompressionFormat.None, 0))); - - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); - PackFileContainerCacheHelper.SaveCache("fp", sourceContainer, dbOptions); - var container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp")!; - // GetFullPath matches by FileName - with duplicates it returns the first DB row - var file = container.FindFile("folder_a\\shared.txt")!; - var path = container.GetFullPath(file); + var file = _container.FindFile("folder_a\\shared.txt")!; + var path = _container.GetFullPath(file); // Should return a valid path (either folder_a or folder_b) Assert.That(path, Does.Contain("shared.txt")); @@ -477,72 +393,20 @@ public void GetFullPath_WithDuplicateFileNames_ReturnsFirstMatch() } [Test] - public void GetDirectoryContent_EmptyContainer_ReturnsEmpty() + public void GetDirectoryContent_UnknownFolder_ReturnsEmpty() { - var sourceContainer = new PackFileContainer("Empty") { SystemFilePath = @"c:\game" }; - - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); - PackFileContainerCacheHelper.SaveCache("fp", sourceContainer, dbOptions); - var container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp")!; - - Assert.That(container.GetDirectoryContent(""), Is.Empty); - Assert.That(container.GetSubDirectories(""), Is.Empty); + Assert.That(_container.GetDirectoryContent("missing\\folder"), Is.Empty); + Assert.That(_container.GetSubDirectories("missing\\folder"), Is.Empty); } } - internal class CachedPackFileContainer_SearchFiles + internal class CachedPackFileContainer_SearchFiles : CachedPackFileContainer_TestBase { - private string _tempDir; - private string _dbFilePath; - private CachedPackFileContainer _container; - - [SetUp] - public void Setup() - { - _tempDir = Path.Combine(Path.GetTempPath(), "CachedSearchTests_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempDir); - _dbFilePath = Path.Combine(_tempDir, "test.db"); - - var sourceContainer = new PackFileContainer("SearchTest") - { - IsCaPackFile = true, - SystemFilePath = @"c:\game\data" - }; - sourceContainer.SourcePackFilePaths.Add(@"c:\game\data\pack1.pack"); - - var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\pack1.pack" }; - sourceContainer.AddOrUpdateFile("models\\unit.rigid_model_v2", new PackFile("unit.rigid_model_v2", - new PackedFileSource(parent, 0, 100, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\vehicle.rigid_model_v2", new PackFile("vehicle.rigid_model_v2", - new PackedFileSource(parent, 100, 200, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\textures\\diffuse.dds", new PackFile("diffuse.dds", - new PackedFileSource(parent, 300, 50, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\textures\\normal.dds", new PackFile("normal.dds", - new PackedFileSource(parent, 350, 50, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("audio\\battle_sound.wem", new PackFile("battle_sound.wem", - new PackedFileSource(parent, 400, 300, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("audio\\music.wem", new PackFile("music.wem", - new PackedFileSource(parent, 700, 150, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("scripts\\campaign_script.lua", new PackFile("campaign_script.lua", - new PackedFileSource(parent, 850, 80, false, false, CompressionFormat.None, 0))); - - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); - PackFileContainerCacheHelper.SaveCache("fp", sourceContainer, dbOptions); - _container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "fp")!; - } - - [TearDown] - public void TearDown() - { - if (Directory.Exists(_tempDir)) - Directory.Delete(_tempDir, true); - } - [Test] public void NullFilters_ReturnsAllFiles() { var results = _container.SearchFiles(null, null); - Assert.That(results.Count, Is.EqualTo(7)); + Assert.That(results.Count, Is.EqualTo(15)); } [Test] @@ -550,7 +414,7 @@ public void TextFilter_MatchesFileName() { var results = _container.SearchFiles("unit", null); Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("unit.rigid_model_v2")); + Assert.That(results[0].File.Name, Is.EqualTo("unit.model")); } [Test] @@ -558,7 +422,7 @@ public void TextFilter_IsCaseInsensitive() { var results = _container.SearchFiles("UNIT", null); Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("unit.rigid_model_v2")); + Assert.That(results[0].File.Name, Is.EqualTo("unit.model")); } [Test] @@ -580,7 +444,7 @@ public void TextFilter_NoMatch_ReturnsEmpty() public void ExtensionFilter_SingleExtension() { var results = _container.SearchFiles(null, [".wem"]); - Assert.That(results.Count, Is.EqualTo(2)); + Assert.That(results.Count, Is.EqualTo(3)); Assert.That(results.All(r => r.File.Name.EndsWith(".wem")), Is.True); } @@ -588,7 +452,7 @@ public void ExtensionFilter_SingleExtension() public void ExtensionFilter_MultipleExtensions() { var results = _container.SearchFiles(null, [".wem", ".lua"]); - Assert.That(results.Count, Is.EqualTo(3)); + Assert.That(results.Count, Is.EqualTo(4)); } [Test] @@ -634,65 +498,20 @@ public void ResultsContainCorrectPaths() public void EmptyExtensionList_TreatedAsNoFilter() { var results = _container.SearchFiles(null, []); - Assert.That(results.Count, Is.EqualTo(7)); + Assert.That(results.Count, Is.EqualTo(15)); } } [TestFixture] - public class CachedPackFileContainer_GetDirectoryContent_PathRows + internal class CachedPackFileContainer_GetDirectoryContent_PathRows : CachedPackFileContainer_TestBase { - private string _tempDir = null!; - private string _dbPath = null!; - private CachedPackFileContainer _container = null!; - - // Test structure (backslash delimited, invariant: every folder has at least one direct file): - // audio\filea - // audio\fileb - // texture\textureFile <- direct file so "texture" sub-folder is discoverable - // texture\mesha\filea - // texture\meshb\filea - // rootfilea - // rootfileb - - [SetUp] - public void SetUp() - { - _tempDir = Path.Combine(Path.GetTempPath(), "QueryDirTests_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempDir); - _dbPath = Path.Combine(_tempDir, "test_cache.db"); - - var sourceContainer = new PackFileContainer("TestCache") { IsCaPackFile = true, SystemFilePath = @"c:\game\data" }; - sourceContainer.SourcePackFilePaths.Add(@"c:\game\data\p.pack"); - var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\p.pack" }; - - sourceContainer.AddOrUpdateFile(@"audio\filea", new PackFile("filea", new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile(@"audio\fileb", new PackFile("fileb", new PackedFileSource(parent, 10, 10, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile(@"texture\textureFile", new PackFile("textureFile", new PackedFileSource(parent, 20, 10, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile(@"texture\mesha\filea", new PackFile("filea", new PackedFileSource(parent, 30, 10, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile(@"texture\meshb\filea", new PackFile("filea", new PackedFileSource(parent, 40, 10, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile(@"rootfilea", new PackFile("rootfilea", new PackedFileSource(parent, 50, 10, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile(@"rootfileb", new PackFile("rootfileb", new PackedFileSource(parent, 60, 10, false, false, CompressionFormat.None, 0))); - - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbPath); - PackFileContainerCacheHelper.SaveCache("test_fp", sourceContainer, dbOptions); - _container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "test_fp")!; - } - - [TearDown] - public void TearDown() - { - if (Directory.Exists(_tempDir)) - Directory.Delete(_tempDir, true); - } - [Test] public void Root_IncludesOnlyRootFiles() { var rows = _container.GetDirectoryContent(""); var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - Assert.That(paths, Does.Contain("rootfilea")); - Assert.That(paths, Does.Contain("rootfileb")); + Assert.That(paths, Does.Contain("root_file.txt")); } [Test] @@ -710,41 +529,42 @@ public void Root_ReturnsExpectedRowCount() { // root contains only its direct files var rows = _container.GetDirectoryContent(""); - Assert.That(rows.Count, Is.EqualTo(2)); + Assert.That(rows.Count, Is.EqualTo(1)); } [Test] public void Subfolder_IncludesOnlyDirectFiles() { - var rows = _container.GetDirectoryContent(@"texture"); + var rows = _container.GetDirectoryContent(@"models\textures"); var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - Assert.That(paths, Does.Contain(@"texture\textureFile")); - Assert.That(paths.Count, Is.EqualTo(1)); + Assert.That(paths, Does.Contain(@"models\textures\diffuse.dds")); + Assert.That(paths, Does.Contain(@"models\textures\normal.dds")); + Assert.That(paths.Count, Is.EqualTo(2)); } [Test] public void Subfolder_ExcludesParentAndSiblingRows() { - var rows = _container.GetDirectoryContent(@"texture"); + var rows = _container.GetDirectoryContent(@"models\textures"); var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); Assert.That(paths.Any(p => p.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.False); - Assert.That(paths.Any(p => !p.StartsWith(@"texture\", StringComparison.OrdinalIgnoreCase)), Is.False); + Assert.That(paths.Any(p => !p.StartsWith(@"models\textures\", StringComparison.OrdinalIgnoreCase)), Is.False); } [Test] public void Subfolder_ReturnsExpectedRowCount() { - var rows = _container.GetDirectoryContent(@"texture"); - Assert.That(rows.Count, Is.EqualTo(1)); + var rows = _container.GetDirectoryContent(@"models\textures"); + Assert.That(rows.Count, Is.EqualTo(2)); } [Test] public void LeafFolder_ReturnsOnlyDirectFiles() { var rows = _container.GetDirectoryContent(@"audio"); - Assert.That(rows.Count, Is.EqualTo(2)); + Assert.That(rows.Count, Is.EqualTo(3)); Assert.That(rows.All(r => r.Path.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.True); } } From a6ed8051c836f9ad7f9991571405e7fa465a5851 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 7 May 2026 21:07:07 +0200 Subject: [PATCH 05/26] code --- .../CachedPackFileContainerTests.cs | 571 ------------------ .../PackFileContainerTests_EdgeCases.cs | 65 ++ ...kFileContainerTests_GetDirectoryContent.cs | 112 ++++ ...ainerTests_GetDirectoryContent_PathRows.cs | 76 +++ .../PackFileContainerTests_ReadOnly.cs | 198 ++++++ .../PackFileContainerTests_SearchFiles.cs | 113 ++++ .../PackFileContainerTests_TestBase.cs | 105 ++++ 7 files changed, 669 insertions(+), 571 deletions(-) delete mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_EdgeCases.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent_PathRows.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ReadOnly.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs deleted file mode 100644 index 2404e3358..000000000 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/CachedPackFileContainerTests.cs +++ /dev/null @@ -1,571 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Shared.Core.PackFiles; -using Shared.Core.PackFiles.Models; -using Shared.Core.PackFiles.Models.Containers; -using Shared.Core.PackFiles.Models.FileSources; -using Shared.Core.PackFiles.Serialization.CacheDatabase; -using Shared.Core.PackFiles.Utility; -using Shared.Core.Settings; - -namespace Shared.CoreTest.PackFiles.Models.Containers -{ - internal abstract class CachedPackFileContainer_TestBase - { - protected string _tempDir = null!; - protected string _dbFilePath = null!; - protected CachedPackFileContainer _container = null!; - - [SetUp] - public void Setup() - { - _tempDir = Path.Combine(Path.GetTempPath(), "CachedContainerTests_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempDir); - _dbFilePath = Path.Combine(_tempDir, "test.db"); - - // Master test dataset used by all fixtures in this file. - var sourceContainer = new PackFileContainer("TestCache") - { - IsCaPackFile = true, - SystemFilePath = @"c:\game\data" - }; - sourceContainer.SourcePackFilePaths.Add(@"c:\game\data\pack1.pack"); - - var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\pack1.pack" }; - sourceContainer.AddOrUpdateFile("folder\\file.txt", new PackFile("file.txt", - new PackedFileSource(parent, 100, 200, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("other\\data.bin", new PackFile("data.bin", - new PackedFileSource(parent, 300, 400, false, true, CompressionFormat.Lz4, 800))); - sourceContainer.AddOrUpdateFile("audio\\sound.wem", new PackFile("sound.wem", - new PackedFileSource(parent, 700, 500, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("root_file.txt", new PackFile("root_file.txt", - new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\unit.model", new PackFile("unit.model", - new PackedFileSource(parent, 10, 20, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\vehicle.model", new PackFile("vehicle.model", - new PackedFileSource(parent, 30, 40, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\textures\\diffuse.dds", new PackFile("diffuse.dds", - new PackedFileSource(parent, 70, 50, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\textures\\normal.dds", new PackFile("normal.dds", - new PackedFileSource(parent, 120, 60, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("models\\textures\\specular\\gloss.dds", new PackFile("gloss.dds", - new PackedFileSource(parent, 180, 30, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("audio\\music.wem", new PackFile("music.wem", - new PackedFileSource(parent, 210, 100, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("audio\\battle_sound.wem", new PackFile("battle_sound.wem", - new PackedFileSource(parent, 400, 300, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("scripts\\campaign_script.lua", new PackFile("campaign_script.lua", - new PackedFileSource(parent, 850, 80, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("folder_a\\shared.txt", new PackFile("shared.txt", - new PackedFileSource(parent, 900, 10, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("folder_b\\shared.txt", new PackFile("shared.txt", - new PackedFileSource(parent, 910, 20, false, false, CompressionFormat.None, 0))); - sourceContainer.AddOrUpdateFile("compressed\\data.bin", new PackFile("data.bin", - new PackedFileSource(parent, 1000, 500, true, true, CompressionFormat.Lz4, 2000))); - - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); - PackFileContainerCacheHelper.SaveCache("test_fp", sourceContainer, dbOptions); - _container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "test_fp")!; - } - - [TearDown] - public void TearDown() - { - if (Directory.Exists(_tempDir)) - Directory.Delete(_tempDir, true); - } - } - - internal class CachedPackFileContainer_ReadOnly : CachedPackFileContainer_TestBase - { - - [Test] - public void IsCaPackFile_AlwaysTrue() - { - Assert.That(_container.IsCaPackFile, Is.True); - } - - [Test] - public void IsCaPackFile_SetterDoesNotChangeValue() - { - _container.IsCaPackFile = false; - Assert.That(_container.IsCaPackFile, Is.True); - } - - [Test] - public void GetFileCount_ReturnsCorrectCount() - { - Assert.That(_container.GetFileCount(), Is.EqualTo(15)); - } - - [Test] - public void FindFile_ReturnsFile() - { - var result = _container.FindFile("folder\\file.txt"); - Assert.That(result, Is.Not.Null); - Assert.That(result.Name, Is.EqualTo("file.txt")); - } - - [Test] - public void FindFile_ReturnsNullForMissing() - { - var result = _container.FindFile("missing\\path.txt"); - Assert.That(result, Is.Null); - } - - [Test] - public void FindFile_NormalizesPath() - { - var result = _container.FindFile("FOLDER/FILE.TXT"); - Assert.That(result, Is.Not.Null); - } - - [Test] - public void FindFile_ReturnsCorrectDataSource() - { - var result = _container.FindFile("folder\\file.txt"); - var source = result!.DataSource as PackedFileSource; - Assert.That(source, Is.Not.Null); - Assert.That(source.Offset, Is.EqualTo(100)); - Assert.That(source.Size, Is.EqualTo(200)); - } - - [Test] - public void ContainsFile_ReturnsTrueForExisting() - { - Assert.That(_container.ContainsFile("folder\\file.txt"), Is.True); - } - - [Test] - public void ContainsFile_ReturnsFalseForMissing() - { - Assert.That(_container.ContainsFile("missing.txt"), Is.False); - } - - [Test] - public void GetFullPath_ReturnsPath() - { - var file = _container.FindFile("folder\\file.txt")!; - var path = _container.GetFullPath(file); - Assert.That(path, Is.EqualTo("folder\\file.txt")); - } - - [Test] - public void GetFullPath_ReturnsNullForMissing() - { - var unknownFile = new PackFile("unknown.txt", null); - var path = _container.GetFullPath(unknownFile); - Assert.That(path, Is.Null); - } - - [Test] - public void FindAllWithExtention_ReturnsMatching() - { - var results = _container.FindAllWithExtention(".wem"); - Assert.That(results.Count, Is.EqualTo(3)); - Assert.That(results[0].FileName, Does.EndWith("sound.wem")); - } - - [Test] - public void FindAllWithExtention_ReturnsEmptyForNoMatch() - { - var results = _container.FindAllWithExtention(".xyz"); - Assert.That(results, Is.Empty); - } - - [Test] - public void GetAllFiles_ReturnsAllFiles() - { - var all = _container.GetAllFiles(); - Assert.That(all.Count, Is.EqualTo(15)); - Assert.That(all.ContainsKey("folder\\file.txt"), Is.True); - Assert.That(all.ContainsKey("other\\data.bin"), Is.True); - Assert.That(all.ContainsKey("audio\\sound.wem"), Is.True); - } - - [Test] - public void AddFiles_Throws() - { - var newFiles = new List - { - new("folder", new PackFile("new.txt", null)) - }; - Assert.Throws(() => _container.AddFiles(newFiles)); - } - - [Test] - public void DeleteFile_Throws() - { - var file = _container.FindFile("folder\\file.txt")!; - Assert.Throws(() => _container.DeleteFile(file)); - } - - [Test] - public void DeleteFolder_Throws() - { - Assert.Throws(() => _container.DeleteFolder("folder")); - } - - [Test] - public void MoveFile_Throws() - { - var file = _container.FindFile("folder\\file.txt")!; - Assert.Throws(() => _container.MoveFile(file, "other")); - } - - [Test] - public void RenameDirectory_Throws() - { - Assert.Throws(() => _container.RenameDirectory("folder", "renamed")); - } - - [Test] - public void RenameFile_Throws() - { - var file = _container.FindFile("folder\\file.txt")!; - Assert.Throws(() => _container.RenameFile(file, "renamed.txt")); - } - - [Test] - public void SaveFileData_Throws() - { - var file = _container.FindFile("folder\\file.txt")!; - Assert.Throws(() => _container.SaveFileData(file, [1, 2, 3])); - } - - [Test] - public void SaveToDisk_Throws() - { - var gameInfo = GameInformationDatabase.GetGameById(GameTypeEnum.Warhammer3); - Assert.Throws(() => _container.SaveToDisk("path", false, gameInfo)); - } - - [Test] - public void AddOrUpdateFile_Throws() - { - Assert.Throws(() => - _container.AddOrUpdateFile("test\\new.txt", new PackFile("new.txt", null))); - } - } - - internal class CachedPackFileContainer_GetDirectoryContent : CachedPackFileContainer_TestBase - { - [Test] - public void Root_ReturnsRootFilesAndTopFolders() - { - var content = PackFileServiceUtility.SplitDirectoryEntries(_container, ""); - - Assert.That(content.Files.Any(f => f.FileName == "root_file.txt"), Is.True); - Assert.That(content.SubFolders, Does.Contain("models")); - Assert.That(content.SubFolders, Does.Contain("audio")); - // Should NOT contain nested folders at root level - Assert.That(content.SubFolders, Does.Not.Contain("textures")); - } - - [Test] - public void Root_DoesNotIncludeFilesFromSubfolders() - { - var entries = _container.GetDirectoryContent(""); - - Assert.That(entries.Count, Is.EqualTo(1)); - Assert.That(entries[0].File.Name, Is.EqualTo("root_file.txt")); - } - - [Test] - public void Subfolder_ReturnsDirectFilesAndImmediateSubfolders() - { - var entries = _container.GetDirectoryContent("models"); - var subFolders = _container.GetSubDirectories("models"); - - Assert.That(entries.Any(f => f.File.Name == "unit.model"), Is.True); - Assert.That(entries.Any(f => f.File.Name == "vehicle.model"), Is.True); - Assert.That(subFolders, Does.Contain("textures")); - Assert.That(entries.Any(f => f.File.Name == "diffuse.dds"), Is.False); - } - - [Test] - public void NestedSubfolder_ReturnsCorrectContent() - { - var entries = _container.GetDirectoryContent("models\\textures"); - var subFolders = _container.GetSubDirectories("models\\textures"); - - Assert.That(entries.Any(f => f.File.Name == "diffuse.dds"), Is.True); - Assert.That(entries.Any(f => f.File.Name == "normal.dds"), Is.True); - Assert.That(subFolders, Does.Contain("specular")); - Assert.That(entries.Count, Is.EqualTo(2)); - } - - [Test] - public void DeepNestedFolder_ReturnsOnlyItsFiles() - { - var entries = _container.GetDirectoryContent("models\\textures\\specular"); - var subFolders = _container.GetSubDirectories("models\\textures\\specular"); - - Assert.That(entries.Count, Is.EqualTo(1)); - Assert.That(entries[0].File.Name, Is.EqualTo("gloss.dds")); - Assert.That(subFolders, Is.Empty); - } - - [Test] - public void NonexistentFolder_ReturnsEmpty() - { - var entries = _container.GetDirectoryContent("nonexistent\\path"); - var subFolders = _container.GetSubDirectories("nonexistent\\path"); - - Assert.That(entries, Is.Empty); - Assert.That(subFolders, Is.Empty); - } - - [Test] - public void SubfoldersAreSorted() - { - var subFolders = _container.GetSubDirectories(""); - - var sorted = subFolders.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList(); - Assert.That(subFolders, Is.EqualTo(sorted)); - } - - [Test] - public void FilesAreSorted() - { - var entries = _container.GetDirectoryContent("models"); - - var sorted = entries.OrderBy(x => x.File.Name, StringComparer.CurrentCultureIgnoreCase).ToList(); - Assert.That(entries.Select(f => f.File.Name), Is.EqualTo(sorted.Select(f => f.File.Name))); - } - - [Test] - public void Files_HaveCorrectDataSource() - { - var entries = _container.GetDirectoryContent("models"); - var unitFile = entries.First(f => f.File.Name == "unit.model"); - var source = unitFile.File.DataSource as PackedFileSource; - - Assert.That(source, Is.Not.Null); - Assert.That(source.Offset, Is.EqualTo(10)); - Assert.That(source.Size, Is.EqualTo(20)); - Assert.That(source.Parent.FilePath, Is.EqualTo(@"c:\game\data\pack1.pack")); - } - } - - internal class CachedPackFileContainer_EdgeCases : CachedPackFileContainer_TestBase - { - [Test] - public void FindAllWithExtention_IsCaseInsensitive() - { - var upper = _container.FindAllWithExtention(".WEM"); - var lower = _container.FindAllWithExtention(".wem"); - Assert.That(upper.Count, Is.EqualTo(3)); - Assert.That(lower.Count, Is.EqualTo(3)); - } - - [Test] - public void ContainsFile_NormalizesForwardSlashes() - { - Assert.That(_container.ContainsFile("folder/file.txt"), Is.True); - Assert.That(_container.ContainsFile("FOLDER/FILE.TXT"), Is.True); - Assert.That(_container.ContainsFile(" folder\\file.txt "), Is.True); - } - - [Test] - public void GetAllFiles_PreservesCompressionMetadata() - { - var all = _container.GetAllFiles(); - var source = (PackedFileSource)all["compressed\\data.bin"].DataSource; - - Assert.That(source.IsEncrypted, Is.True); - Assert.That(source.IsCompressed, Is.True); - Assert.That(source.CompressionFormat, Is.EqualTo(CompressionFormat.Lz4)); - Assert.That(source.UncompressedSize, Is.EqualTo(2000)); - Assert.That(source.Offset, Is.EqualTo(1000)); - Assert.That(source.Size, Is.EqualTo(500)); - } - - [Test] - public void GetFullPath_WithDuplicateFileNames_ReturnsFirstMatch() - { - // GetFullPath matches by FileName - with duplicates it returns the first DB row - var file = _container.FindFile("folder_a\\shared.txt")!; - var path = _container.GetFullPath(file); - - // Should return a valid path (either folder_a or folder_b) - Assert.That(path, Does.Contain("shared.txt")); - Assert.That(path, Does.Contain("\\")); - } - - [Test] - public void GetDirectoryContent_UnknownFolder_ReturnsEmpty() - { - Assert.That(_container.GetDirectoryContent("missing\\folder"), Is.Empty); - Assert.That(_container.GetSubDirectories("missing\\folder"), Is.Empty); - } - } - - internal class CachedPackFileContainer_SearchFiles : CachedPackFileContainer_TestBase - { - [Test] - public void NullFilters_ReturnsAllFiles() - { - var results = _container.SearchFiles(null, null); - Assert.That(results.Count, Is.EqualTo(15)); - } - - [Test] - public void TextFilter_MatchesFileName() - { - var results = _container.SearchFiles("unit", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("unit.model")); - } - - [Test] - public void TextFilter_IsCaseInsensitive() - { - var results = _container.SearchFiles("UNIT", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("unit.model")); - } - - [Test] - public void TextFilter_PartialMatch() - { - var results = _container.SearchFiles("battle", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); - } - - [Test] - public void TextFilter_NoMatch_ReturnsEmpty() - { - var results = _container.SearchFiles("nonexistent", null); - Assert.That(results, Is.Empty); - } - - [Test] - public void ExtensionFilter_SingleExtension() - { - var results = _container.SearchFiles(null, [".wem"]); - Assert.That(results.Count, Is.EqualTo(3)); - Assert.That(results.All(r => r.File.Name.EndsWith(".wem")), Is.True); - } - - [Test] - public void ExtensionFilter_MultipleExtensions() - { - var results = _container.SearchFiles(null, [".wem", ".lua"]); - Assert.That(results.Count, Is.EqualTo(4)); - } - - [Test] - public void ExtensionFilter_NoMatch_ReturnsEmpty() - { - var results = _container.SearchFiles(null, [".xyz"]); - Assert.That(results, Is.Empty); - } - - [Test] - public void CombinedFilters_TextAndExtension() - { - var results = _container.SearchFiles("battle", [".wem"]); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); - } - - [Test] - public void CombinedFilters_TextMatchesButExtensionDoesNot_ReturnsEmpty() - { - var results = _container.SearchFiles("unit", [".wem"]); - Assert.That(results, Is.Empty); - } - - [Test] - public void ResultsAreSortedByPath() - { - var results = _container.SearchFiles(null, null); - var paths = results.Select(r => r.Path).ToList(); - var sorted = paths.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToList(); - Assert.That(paths, Is.EqualTo(sorted)); - } - - [Test] - public void ResultsContainCorrectPaths() - { - var results = _container.SearchFiles("diffuse", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].Path, Is.EqualTo("models\\textures\\diffuse.dds")); - } - - [Test] - public void EmptyExtensionList_TreatedAsNoFilter() - { - var results = _container.SearchFiles(null, []); - Assert.That(results.Count, Is.EqualTo(15)); - } - } - - [TestFixture] - internal class CachedPackFileContainer_GetDirectoryContent_PathRows : CachedPackFileContainer_TestBase - { - [Test] - public void Root_IncludesOnlyRootFiles() - { - var rows = _container.GetDirectoryContent(""); - var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - - Assert.That(paths, Does.Contain("root_file.txt")); - } - - [Test] - public void Root_ExcludesDeepDescendantFolders() - { - var rows = _container.GetDirectoryContent(""); - var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - - Assert.That(paths, Does.Not.Contain(@"texture\mesha\filea")); - Assert.That(paths, Does.Not.Contain(@"texture\meshb\filea")); - } - - [Test] - public void Root_ReturnsExpectedRowCount() - { - // root contains only its direct files - var rows = _container.GetDirectoryContent(""); - Assert.That(rows.Count, Is.EqualTo(1)); - } - - [Test] - public void Subfolder_IncludesOnlyDirectFiles() - { - var rows = _container.GetDirectoryContent(@"models\textures"); - var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - - Assert.That(paths, Does.Contain(@"models\textures\diffuse.dds")); - Assert.That(paths, Does.Contain(@"models\textures\normal.dds")); - Assert.That(paths.Count, Is.EqualTo(2)); - } - - [Test] - public void Subfolder_ExcludesParentAndSiblingRows() - { - var rows = _container.GetDirectoryContent(@"models\textures"); - var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - - Assert.That(paths.Any(p => p.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.False); - Assert.That(paths.Any(p => !p.StartsWith(@"models\textures\", StringComparison.OrdinalIgnoreCase)), Is.False); - } - - [Test] - public void Subfolder_ReturnsExpectedRowCount() - { - var rows = _container.GetDirectoryContent(@"models\textures"); - Assert.That(rows.Count, Is.EqualTo(2)); - } - - [Test] - public void LeafFolder_ReturnsOnlyDirectFiles() - { - var rows = _container.GetDirectoryContent(@"audio"); - Assert.That(rows.Count, Is.EqualTo(3)); - Assert.That(rows.All(r => r.Path.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.True); - } - } -} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_EdgeCases.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_EdgeCases.cs new file mode 100644 index 000000000..e9380a279 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_EdgeCases.cs @@ -0,0 +1,65 @@ +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.PackFiles.Utility; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_EdgeCases : PackFileContainerTests_TestBase + { + public PackFileContainerTests_EdgeCases(Type containerType) + : base(containerType) + { + } + + [Test] + public void FindAllWithExtention_IsCaseInsensitive() + { + var upper = _container.FindAllWithExtention(".WEM"); + var lower = _container.FindAllWithExtention(".wem"); + Assert.That(upper.Count, Is.EqualTo(3)); + Assert.That(lower.Count, Is.EqualTo(3)); + } + + [Test] + public void ContainsFile_NormalizesForwardSlashes() + { + Assert.That(_container.ContainsFile("folder/file.txt"), Is.True); + Assert.That(_container.ContainsFile("FOLDER/FILE.TXT"), Is.True); + Assert.That(_container.ContainsFile(" folder\\file.txt "), Is.True); + } + + [Test] + public void GetAllFiles_PreservesCompressionMetadata() + { + var all = _container.GetAllFiles(); + var source = (PackedFileSource)all["compressed\\data.bin"].DataSource; + + Assert.That(source.IsEncrypted, Is.True); + Assert.That(source.IsCompressed, Is.True); + Assert.That(source.CompressionFormat, Is.EqualTo(CompressionFormat.Lz4)); + Assert.That(source.UncompressedSize, Is.EqualTo(2000)); + Assert.That(source.Offset, Is.EqualTo(1000)); + Assert.That(source.Size, Is.EqualTo(500)); + } + + [Test] + public void GetFullPath_WithDuplicateFileNames_ReturnsFirstMatch() + { + var file = _container.FindFile("folder_a\\shared.txt")!; + var path = _container.GetFullPath(file); + + Assert.That(path, Does.Contain("shared.txt")); + Assert.That(path, Does.Contain("\\")); + } + + [Test] + public void GetDirectoryContent_UnknownFolder_ReturnsEmpty() + { + Assert.That(_container.GetDirectoryContent("missing\\folder"), Is.Empty); + Assert.That(_container.GetSubDirectories("missing\\folder"), Is.Empty); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent.cs new file mode 100644 index 000000000..35427ddf1 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent.cs @@ -0,0 +1,112 @@ +using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.PackFiles.Utility; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_GetDirectoryContent : PackFileContainerTests_TestBase + { + public PackFileContainerTests_GetDirectoryContent(Type containerType) + : base(containerType) + { + } + + [Test] + public void Root_ReturnsRootFilesAndTopFolders() + { + var content = PackFileServiceUtility.SplitDirectoryEntries(_container, ""); + + Assert.That(content.Files.Any(f => f.FileName == "root_file.txt"), Is.True); + Assert.That(content.SubFolders, Does.Contain("models")); + Assert.That(content.SubFolders, Does.Contain("audio")); + Assert.That(content.SubFolders, Does.Not.Contain("textures")); + } + + [Test] + public void Root_DoesNotIncludeFilesFromSubfolders() + { + var entries = _container.GetDirectoryContent(""); + + Assert.That(entries.Count, Is.EqualTo(1)); + Assert.That(entries[0].File.Name, Is.EqualTo("root_file.txt")); + } + + [Test] + public void Subfolder_ReturnsDirectFilesAndImmediateSubfolders() + { + var entries = _container.GetDirectoryContent("models"); + var subFolders = _container.GetSubDirectories("models"); + + Assert.That(entries.Any(f => f.File.Name == "unit.model"), Is.True); + Assert.That(entries.Any(f => f.File.Name == "vehicle.model"), Is.True); + Assert.That(subFolders, Does.Contain("textures")); + Assert.That(entries.Any(f => f.File.Name == "diffuse.dds"), Is.False); + } + + [Test] + public void NestedSubfolder_ReturnsCorrectContent() + { + var entries = _container.GetDirectoryContent("models\\textures"); + var subFolders = _container.GetSubDirectories("models\\textures"); + + Assert.That(entries.Any(f => f.File.Name == "diffuse.dds"), Is.True); + Assert.That(entries.Any(f => f.File.Name == "normal.dds"), Is.True); + Assert.That(subFolders, Does.Contain("specular")); + Assert.That(entries.Count, Is.EqualTo(2)); + } + + [Test] + public void DeepNestedFolder_ReturnsOnlyItsFiles() + { + var entries = _container.GetDirectoryContent("models\\textures\\specular"); + var subFolders = _container.GetSubDirectories("models\\textures\\specular"); + + Assert.That(entries.Count, Is.EqualTo(1)); + Assert.That(entries[0].File.Name, Is.EqualTo("gloss.dds")); + Assert.That(subFolders, Is.Empty); + } + + [Test] + public void NonexistentFolder_ReturnsEmpty() + { + var entries = _container.GetDirectoryContent("nonexistent\\path"); + var subFolders = _container.GetSubDirectories("nonexistent\\path"); + + Assert.That(entries, Is.Empty); + Assert.That(subFolders, Is.Empty); + } + + [Test] + public void SubfoldersAreSorted() + { + var subFolders = _container.GetSubDirectories(""); + + var sorted = subFolders.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList(); + Assert.That(subFolders, Is.EqualTo(sorted)); + } + + [Test] + public void FilesAreSorted() + { + var entries = _container.GetDirectoryContent("models"); + + var sorted = entries.OrderBy(x => x.File.Name, StringComparer.CurrentCultureIgnoreCase).ToList(); + Assert.That(entries.Select(f => f.File.Name), Is.EqualTo(sorted.Select(f => f.File.Name))); + } + + [Test] + public void Files_HaveCorrectDataSource() + { + var entries = _container.GetDirectoryContent("models"); + var unitFile = entries.First(f => f.File.Name == "unit.model"); + var source = unitFile.File.DataSource as PackedFileSource; + + Assert.That(source, Is.Not.Null); + Assert.That(source.Offset, Is.EqualTo(10)); + Assert.That(source.Size, Is.EqualTo(20)); + Assert.That(source.Parent.FilePath, Is.EqualTo(@"c:\game\data\pack1.pack")); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent_PathRows.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent_PathRows.cs new file mode 100644 index 000000000..799a8caf2 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent_PathRows.cs @@ -0,0 +1,76 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_GetDirectoryContent_PathRows : PackFileContainerTests_TestBase + { + public PackFileContainerTests_GetDirectoryContent_PathRows(Type containerType) + : base(containerType) + { + } + + [Test] + public void Root_IncludesOnlyRootFiles() + { + var rows = _container.GetDirectoryContent(""); + var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.That(paths, Does.Contain("root_file.txt")); + } + + [Test] + public void Root_ExcludesDeepDescendantFolders() + { + var rows = _container.GetDirectoryContent(""); + var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.That(paths, Does.Not.Contain(@"texture\mesha\filea")); + Assert.That(paths, Does.Not.Contain(@"texture\meshb\filea")); + } + + [Test] + public void Root_ReturnsExpectedRowCount() + { + var rows = _container.GetDirectoryContent(""); + Assert.That(rows.Count, Is.EqualTo(1)); + } + + [Test] + public void Subfolder_IncludesOnlyDirectFiles() + { + var rows = _container.GetDirectoryContent(@"models\textures"); + var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.That(paths, Does.Contain(@"models\textures\diffuse.dds")); + Assert.That(paths, Does.Contain(@"models\textures\normal.dds")); + Assert.That(paths.Count, Is.EqualTo(2)); + } + + [Test] + public void Subfolder_ExcludesParentAndSiblingRows() + { + var rows = _container.GetDirectoryContent(@"models\textures"); + var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.That(paths.Any(p => p.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.False); + Assert.That(paths.Any(p => !p.StartsWith(@"models\textures\", StringComparison.OrdinalIgnoreCase)), Is.False); + } + + [Test] + public void Subfolder_ReturnsExpectedRowCount() + { + var rows = _container.GetDirectoryContent(@"models\textures"); + Assert.That(rows.Count, Is.EqualTo(2)); + } + + [Test] + public void LeafFolder_ReturnsOnlyDirectFiles() + { + var rows = _container.GetDirectoryContent(@"audio"); + Assert.That(rows.Count, Is.EqualTo(3)); + Assert.That(rows.All(r => r.Path.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.True); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ReadOnly.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ReadOnly.cs new file mode 100644 index 000000000..c3b5dfbca --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ReadOnly.cs @@ -0,0 +1,198 @@ +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.Settings; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_ReadOnly : PackFileContainerTests_TestBase + { + public PackFileContainerTests_ReadOnly(Type containerType) + : base(containerType) + { + } + + [Test] + public void IsCaPackFile_AlwaysTrue() + { + Assert.That(_container.IsCaPackFile, Is.True); + } + + [Test] + public void IsCaPackFile_SetterDoesNotChangeValue() + { + _container.IsCaPackFile = false; + if (IsCachedContainer) + Assert.That(_container.IsCaPackFile, Is.True); + else + Assert.That(_container.IsCaPackFile, Is.False); + } + + [Test] + public void GetFileCount_ReturnsCorrectCount() + { + Assert.That(_container.GetFileCount(), Is.EqualTo(15)); + } + + [Test] + public void FindFile_ReturnsFile() + { + var result = _container.FindFile("folder\\file.txt"); + Assert.That(result, Is.Not.Null); + Assert.That(result.Name, Is.EqualTo("file.txt")); + } + + [Test] + public void FindFile_ReturnsNullForMissing() + { + var result = _container.FindFile("missing\\path.txt"); + Assert.That(result, Is.Null); + } + + [Test] + public void FindFile_NormalizesPath() + { + var result = _container.FindFile("FOLDER/FILE.TXT"); + Assert.That(result, Is.Not.Null); + } + + [Test] + public void FindFile_ReturnsCorrectDataSource() + { + var result = _container.FindFile("folder\\file.txt"); + var source = result!.DataSource as PackedFileSource; + Assert.That(source, Is.Not.Null); + Assert.That(source.Offset, Is.EqualTo(100)); + Assert.That(source.Size, Is.EqualTo(200)); + } + + [Test] + public void ContainsFile_ReturnsTrueForExisting() + { + Assert.That(_container.ContainsFile("folder\\file.txt"), Is.True); + } + + [Test] + public void ContainsFile_ReturnsFalseForMissing() + { + Assert.That(_container.ContainsFile("missing.txt"), Is.False); + } + + [Test] + public void GetFullPath_ReturnsPath() + { + var file = _container.FindFile("folder\\file.txt")!; + var path = _container.GetFullPath(file); + Assert.That(path, Is.EqualTo("folder\\file.txt")); + } + + [Test] + public void GetFullPath_ReturnsNullForMissing() + { + var unknownFile = new PackFile("unknown.txt", null); + var path = _container.GetFullPath(unknownFile); + Assert.That(path, Is.Null); + } + + [Test] + public void FindAllWithExtention_ReturnsMatching() + { + var results = _container.FindAllWithExtention(".wem"); + Assert.That(results.Count, Is.EqualTo(3)); + Assert.That(results[0].FileName, Does.EndWith("sound.wem")); + } + + [Test] + public void FindAllWithExtention_ReturnsEmptyForNoMatch() + { + var results = _container.FindAllWithExtention(".xyz"); + Assert.That(results, Is.Empty); + } + + [Test] + public void GetAllFiles_ReturnsAllFiles() + { + var all = _container.GetAllFiles(); + Assert.That(all.Count, Is.EqualTo(15)); + Assert.That(all.ContainsKey("folder\\file.txt"), Is.True); + Assert.That(all.ContainsKey("other\\data.bin"), Is.True); + Assert.That(all.ContainsKey("audio\\sound.wem"), Is.True); + } + + [Test] + public void AddFiles_Throws() + { + var cachedContainer = GetCachedContainerOrIgnore(nameof(AddFiles_Throws)); + var newFiles = new List + { + new("folder", new PackFile("new.txt", null)) + }; + Assert.Throws(() => cachedContainer.AddFiles(newFiles)); + } + + [Test] + public void DeleteFile_Throws() + { + var cachedContainer = GetCachedContainerOrIgnore(nameof(DeleteFile_Throws)); + var file = _container.FindFile("folder\\file.txt")!; + Assert.Throws(() => cachedContainer.DeleteFile(file)); + } + + [Test] + public void DeleteFolder_Throws() + { + var cachedContainer = GetCachedContainerOrIgnore(nameof(DeleteFolder_Throws)); + Assert.Throws(() => cachedContainer.DeleteFolder("folder")); + } + + [Test] + public void MoveFile_Throws() + { + var cachedContainer = GetCachedContainerOrIgnore(nameof(MoveFile_Throws)); + var file = _container.FindFile("folder\\file.txt")!; + Assert.Throws(() => cachedContainer.MoveFile(file, "other")); + } + + [Test] + public void RenameDirectory_Throws() + { + var cachedContainer = GetCachedContainerOrIgnore(nameof(RenameDirectory_Throws)); + Assert.Throws(() => cachedContainer.RenameDirectory("folder", "renamed")); + } + + [Test] + public void RenameFile_Throws() + { + var cachedContainer = GetCachedContainerOrIgnore(nameof(RenameFile_Throws)); + var file = _container.FindFile("folder\\file.txt")!; + Assert.Throws(() => cachedContainer.RenameFile(file, "renamed.txt")); + } + + [Test] + public void SaveFileData_Throws() + { + var cachedContainer = GetCachedContainerOrIgnore(nameof(SaveFileData_Throws)); + var file = _container.FindFile("folder\\file.txt")!; + Assert.Throws(() => cachedContainer.SaveFileData(file, [1, 2, 3])); + } + + [Test] + public void SaveToDisk_Throws() + { + var cachedContainer = GetCachedContainerOrIgnore(nameof(SaveToDisk_Throws)); + var gameInfo = GameInformationDatabase.GetGameById(GameTypeEnum.Warhammer3); + Assert.Throws(() => cachedContainer.SaveToDisk("path", false, gameInfo)); + } + + [Test] + public void AddOrUpdateFile_Throws() + { + IgnoreIfNotCached(nameof(AddOrUpdateFile_Throws)); + Assert.Throws(() => + _container.AddOrUpdateFile("test\\new.txt", new PackFile("new.txt", null))); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs new file mode 100644 index 000000000..7e5c3cdd8 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs @@ -0,0 +1,113 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_SearchFiles : PackFileContainerTests_TestBase + { + public PackFileContainerTests_SearchFiles(Type containerType) + : base(containerType) + { + } + + [Test] + public void NullFilters_ReturnsAllFiles() + { + var results = _container.SearchFiles(null, null); + Assert.That(results.Count, Is.EqualTo(15)); + } + + [Test] + public void TextFilter_MatchesFileName() + { + var results = _container.SearchFiles("unit", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("unit.model")); + } + + [Test] + public void TextFilter_IsCaseInsensitive() + { + var results = _container.SearchFiles("UNIT", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("unit.model")); + } + + [Test] + public void TextFilter_PartialMatch() + { + var results = _container.SearchFiles("battle", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); + } + + [Test] + public void TextFilter_NoMatch_ReturnsEmpty() + { + var results = _container.SearchFiles("nonexistent", null); + Assert.That(results, Is.Empty); + } + + [Test] + public void ExtensionFilter_SingleExtension() + { + var results = _container.SearchFiles(null, [".wem"]); + Assert.That(results.Count, Is.EqualTo(3)); + Assert.That(results.All(r => r.File.Name.EndsWith(".wem")), Is.True); + } + + [Test] + public void ExtensionFilter_MultipleExtensions() + { + var results = _container.SearchFiles(null, [".wem", ".lua"]); + Assert.That(results.Count, Is.EqualTo(4)); + } + + [Test] + public void ExtensionFilter_NoMatch_ReturnsEmpty() + { + var results = _container.SearchFiles(null, [".xyz"]); + Assert.That(results, Is.Empty); + } + + [Test] + public void CombinedFilters_TextAndExtension() + { + var results = _container.SearchFiles("battle", [".wem"]); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); + } + + [Test] + public void CombinedFilters_TextMatchesButExtensionDoesNot_ReturnsEmpty() + { + var results = _container.SearchFiles("unit", [".wem"]); + Assert.That(results, Is.Empty); + } + + [Test] + public void ResultsAreSortedByPath() + { + var results = _container.SearchFiles(null, null); + var paths = results.Select(r => r.Path).ToList(); + var sorted = paths.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToList(); + Assert.That(paths, Is.EqualTo(sorted)); + } + + [Test] + public void ResultsContainCorrectPaths() + { + var results = _container.SearchFiles("diffuse", null); + Assert.That(results.Count, Is.EqualTo(1)); + Assert.That(results[0].Path, Is.EqualTo("models\\textures\\diffuse.dds")); + } + + [Test] + public void EmptyExtensionList_TreatedAsNoFilter() + { + var results = _container.SearchFiles(null, []); + Assert.That(results.Count, Is.EqualTo(15)); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs new file mode 100644 index 000000000..ea2a06062 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs @@ -0,0 +1,105 @@ +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.PackFiles.Serialization.CacheDatabase; +using Shared.Core.PackFiles.Utility; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + internal abstract class PackFileContainerTests_TestBase + { + private readonly bool _useCachedContainer; + + protected string _tempDir = null!; + protected string _dbFilePath = null!; + protected IPackFileContainerInternal _container = null!; + + protected PackFileContainerTests_TestBase(Type containerType) + { + _useCachedContainer = containerType == typeof(CachedPackFileContainer); + } + + protected bool IsCachedContainer => _useCachedContainer; + + [SetUp] + public void Setup() + { + _tempDir = Path.Combine(Path.GetTempPath(), "CachedContainerTests_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + _dbFilePath = Path.Combine(_tempDir, "test.db"); + + var sourceContainer = new PackFileContainer("TestCache") + { + IsCaPackFile = true, + SystemFilePath = @"c:\game\data" + }; + sourceContainer.SourcePackFilePaths.Add(@"c:\game\data\pack1.pack"); + + var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\pack1.pack" }; + sourceContainer.AddOrUpdateFile("folder\\file.txt", new PackFile("file.txt", + new PackedFileSource(parent, 100, 200, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("other\\data.bin", new PackFile("data.bin", + new PackedFileSource(parent, 300, 400, false, true, CompressionFormat.Lz4, 800))); + sourceContainer.AddOrUpdateFile("audio\\sound.wem", new PackFile("sound.wem", + new PackedFileSource(parent, 700, 500, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("root_file.txt", new PackFile("root_file.txt", + new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\unit.model", new PackFile("unit.model", + new PackedFileSource(parent, 10, 20, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\vehicle.model", new PackFile("vehicle.model", + new PackedFileSource(parent, 30, 40, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\textures\\diffuse.dds", new PackFile("diffuse.dds", + new PackedFileSource(parent, 70, 50, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\textures\\normal.dds", new PackFile("normal.dds", + new PackedFileSource(parent, 120, 60, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("models\\textures\\specular\\gloss.dds", new PackFile("gloss.dds", + new PackedFileSource(parent, 180, 30, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("audio\\music.wem", new PackFile("music.wem", + new PackedFileSource(parent, 210, 100, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("audio\\battle_sound.wem", new PackFile("battle_sound.wem", + new PackedFileSource(parent, 400, 300, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("scripts\\campaign_script.lua", new PackFile("campaign_script.lua", + new PackedFileSource(parent, 850, 80, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("folder_a\\shared.txt", new PackFile("shared.txt", + new PackedFileSource(parent, 900, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("folder_b\\shared.txt", new PackFile("shared.txt", + new PackedFileSource(parent, 910, 20, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("compressed\\data.bin", new PackFile("data.bin", + new PackedFileSource(parent, 1000, 500, true, true, CompressionFormat.Lz4, 2000))); + + var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); + if (_useCachedContainer) + { + PackFileContainerCacheHelper.SaveCache("test_fp", sourceContainer, dbOptions); + _container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "test_fp")!; + } + else + { + _container = sourceContainer; + } + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_tempDir)) + Directory.Delete(_tempDir, true); + } + + protected void IgnoreIfNotCached(string scenario) + { + if (!IsCachedContainer) + Assert.Ignore($"{scenario} is currently validated only for CachedPackFileContainer."); + } + + protected CachedPackFileContainer GetCachedContainerOrIgnore(string scenario) + { + if (_container is CachedPackFileContainer cachedContainer) + return cachedContainer; + + Assert.Ignore($"{scenario} requires CachedPackFileContainer-specific API not present on IPackFileContainerInternal."); + return null!; + } + } +} From d7f8417a5dcaf02d73797f5eccb70acc26093f71 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 7 May 2026 21:08:19 +0200 Subject: [PATCH 06/26] Code --- .../Models/Containers/PackFileContainerTests_TestBase.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs index ea2a06062..9348c39f0 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs @@ -1,5 +1,4 @@ -using Shared.Core.PackFiles; -using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.Containers; using Shared.Core.PackFiles.Models.FileSources; using Shared.Core.PackFiles.Serialization.CacheDatabase; @@ -14,14 +13,13 @@ internal abstract class PackFileContainerTests_TestBase protected string _tempDir = null!; protected string _dbFilePath = null!; protected IPackFileContainerInternal _container = null!; + protected bool IsCachedContainer => _useCachedContainer; protected PackFileContainerTests_TestBase(Type containerType) { _useCachedContainer = containerType == typeof(CachedPackFileContainer); } - protected bool IsCachedContainer => _useCachedContainer; - [SetUp] public void Setup() { From bde77e47dd8e164f9db4e0294d13be7e248b0727 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 08:04:44 +0200 Subject: [PATCH 07/26] Code --- .../PackFiles/Models/IPackFileContainer.cs | 11 +- .../Models/IPackFileContainerInternal.cs | 9 +- .../Shared.Core/PackFiles/PackFileService.cs | 13 +- .../Utility/PackFileServiceUtility.cs | 16 +- .../Containers/PackFileContainerTests.cs | 680 ------------------ .../PackFileContainerTests_AddFiles.cs | 48 ++ .../PackFileContainerTests_AddOrUpdateFile.cs | 41 ++ .../PackFileContainerTests_ContainsFile.cs | 23 + .../PackFileContainerTests_DeleteFile.cs | 41 ++ .../PackFileContainerTests_DeleteFolder.cs | 25 + .../PackFileContainerTests_EdgeCases.cs | 65 -- ...FileContainerTests_FindAllWithExtention.cs | 24 + .../PackFileContainerTests_FindFile.cs | 32 + .../PackFileContainerTests_GetAllFiles.cs | 30 + ...kFileContainerTests_GetDirectoryContent.cs | 103 +-- ...ainerTests_GetDirectoryContent_PathRows.cs | 76 -- .../PackFileContainerTests_GetFileCount.cs | 17 + .../PackFileContainerTests_GetFullPath.cs | 27 + ...ackFileContainerTests_GetSubDirectories.cs | 35 + .../PackFileContainerTests_IsCaPackFile.cs | 24 + .../PackFileContainerTests_MoveFile.cs | 27 + .../Containers/PackFileContainerTests_Name.cs | 17 + .../PackFileContainerTests_ReadOnly.cs | 198 ----- .../PackFileContainerTests_RenameDirectory.cs | 25 + .../PackFileContainerTests_RenameFile.cs | 27 + .../PackFileContainerTests_SaveFileData.cs | 30 + .../PackFileContainerTests_SaveToDisk.cs | 22 + .../PackFileContainerTests_SearchFiles.cs | 88 +-- .../PackFileContainerTests_SystemFilePath.cs | 17 + .../PackFileBrowserViewModelTests.cs | 5 +- 30 files changed, 593 insertions(+), 1203 deletions(-) delete mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_AddFiles.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_AddOrUpdateFile.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ContainsFile.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_DeleteFile.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_DeleteFolder.cs delete mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_EdgeCases.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_FindAllWithExtention.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_FindFile.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetAllFiles.cs delete mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent_PathRows.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFileCount.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFullPath.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetSubDirectories.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_IsCaPackFile.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_MoveFile.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_Name.cs delete mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ReadOnly.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_RenameDirectory.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_RenameFile.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SaveFileData.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SaveToDisk.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SystemFilePath.cs diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs index 007d76ff1..0742cd51f 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Shared.Core.PackFiles.Models +namespace Shared.Core.PackFiles.Models { public interface IPackFileContainer { @@ -13,11 +11,10 @@ public interface IPackFileContainer PackFile? FindFile(string path); bool ContainsFile(string path); string? GetFullPath(PackFile file); - void AddOrUpdateFile(string path, PackFile file); - List<(string FileName, PackFile Pack)> FindAllWithExtention(string extention); + Dictionary GetAllFiles(); - List<(string Path, PackFile File)> GetDirectoryContent(string directoryPath); - List GetSubDirectories(string directoryPath); + + List<(string Path, PackFile File)> SearchFiles(string? textFilter, IReadOnlyList? extensions); } } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainerInternal.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainerInternal.cs index c48cf3de6..e1cc13812 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainerInternal.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainerInternal.cs @@ -1,9 +1,10 @@ -using Shared.Core.Settings; +using Shared.Core.Settings; namespace Shared.Core.PackFiles.Models { internal interface IPackFileContainerInternal : IPackFileContainer { + void AddOrUpdateFile(string path, PackFile file); List AddFiles(List newFiles); PackFile? DeleteFile(PackFile file); void DeleteFolder(string folder); @@ -12,5 +13,9 @@ internal interface IPackFileContainerInternal : IPackFileContainer void RenameFile(PackFile file, string newName); void SaveFileData(PackFile file, byte[] data); void SaveToDisk(string path, bool createBackup, GameInformation gameInformation); + + List<(string FileName, PackFile Pack)> FindAllWithExtention(string extention); + List GetSubDirectories(string directoryPath); + List<(string Path, PackFile File)> GetDirectoryContent(string directoryPath); } -} \ No newline at end of file +} diff --git a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs index 6b6ac60e9..4ef75a356 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs @@ -29,7 +29,7 @@ public PackFileService(IGlobalEventHub? globalEventHub) _globalEventHub = globalEventHub; } - private static IPackFileContainerInternal CastContainer(IPackFileContainer container) => (IPackFileContainerInternal)container; + internal static IPackFileContainerInternal CastContainer(IPackFileContainer container) => (IPackFileContainerInternal)container; public List GetAllPackfileContainers() => _packFileContainers.Cast().ToList(); @@ -142,14 +142,17 @@ public void UnloadPackContainer(IPackFileContainer pf) _globalEventHub?.PublishGlobalEvent(new PackFileContainerRemovedEvent(container)); } - public List<(string FileName, PackFile Pack)> FindAllWithExtention(string extention, IPackFileContainer? container = null) + public List<(string FileName, PackFile Pack)> FindAllWithExtention(string extention, IPackFileContainer? pf = null) { - if (container != null) + if (pf != null) + { + var container = CastContainer(pf); return container.FindAllWithExtention(extention); + } var output = new List<(string, PackFile)>(); - foreach (var pf in _packFileContainers) - output.AddRange(pf.FindAllWithExtention(extention)); + foreach (var instance in _packFileContainers) + output.AddRange(instance.FindAllWithExtention(extention)); return output; } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs index 3c48c1f09..4b6c0c5a3 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs @@ -13,12 +13,14 @@ public static class PackFileServiceUtility { public static DirectoryEntriesSplitResult SplitDirectoryEntries(IPackFileContainer container, string directoryPath) { - var files = container.GetDirectoryContent(directoryPath) + var packFileContainerInternal = PackFileService.CastContainer(container); + + var files = packFileContainerInternal.GetDirectoryContent(directoryPath) .Select(entry => new DirectoryFileEntry(entry.File.Name, entry.File)) .OrderBy(x => x.FileName, StringComparer.CurrentCultureIgnoreCase) .ToList(); - var subFolders = container.GetSubDirectories(directoryPath) + var subFolders = packFileContainerInternal.GetSubDirectories(directoryPath) .OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase) .ToList(); @@ -46,11 +48,17 @@ public static List FindAllWithExtention(IPackFileService pfs, string e public static List<(string FileName, PackFile Pack)> FindAllWithExtentionIncludePaths(IPackFileService pfs, string extention, IPackFileContainer? packFileContainer = null) { if (packFileContainer != null) - return packFileContainer.FindAllWithExtention(extention); + { + var container = PackFileService.CastContainer(packFileContainer); + return container.FindAllWithExtention(extention); + } var output = new List<(string, PackFile)>(); foreach (var pf in pfs.GetAllPackfileContainers()) - output.AddRange(pf.FindAllWithExtention(extention)); + { + var container = PackFileService.CastContainer(pf); + output.AddRange(container.FindAllWithExtention(extention)); + } return output; } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs deleted file mode 100644 index d7047a663..000000000 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests.cs +++ /dev/null @@ -1,680 +0,0 @@ -using Shared.Core.PackFiles; -using Shared.Core.PackFiles.Models; -using Shared.Core.PackFiles.Models.Containers; -using Shared.Core.PackFiles.Models.FileSources; -using Shared.Core.PackFiles.Utility; - -namespace Shared.CoreTest.PackFiles.Models.Containers -{ - internal class PackFileContainer_AddFiles - { - [Test] - public void AddFiles_MultipleFiles() - { - var container = new PackFileContainer("Test"); - var newFiles = new List - { - new("Directory_0", new PackFile("file0.txt", null)), - new("Directory_0", new PackFile("file1.txt", null)), - new("Directory_1", new PackFile("file0.txt", null)), - new("", new PackFile("rootFile.txt", null)) - }; - - var result = container.AddFiles(newFiles); - - Assert.That(container.GetFileCount(), Is.EqualTo(4)); - Assert.That(result.Count, Is.EqualTo(4)); - } - - [Test] - public void AddFiles_AddToRoot() - { - var container = new PackFileContainer("Test"); - var newFiles = new List - { - new("", new PackFile("rootFile.txt", null)) - }; - - container.AddFiles(newFiles); - - Assert.That(container.GetFileCount(), Is.EqualTo(1)); - Assert.That(container.ContainsFile("rootfile.txt"), Is.True); - } - - [Test] - public void AddFiles_AddToFolder() - { - var container = new PackFileContainer("Test"); - var newFiles = new List - { - new("Directory_0", new PackFile("file0.txt", null)), - new("Directory_0", new PackFile("file1.txt", null)), - new("Directory_1", new PackFile("file0.txt", null)), - }; - - container.AddFiles(newFiles); - - Assert.That(container.GetFileCount(), Is.EqualTo(3)); - } - - [Test] - public void AddFiles_FileNameConflict_Override() - { - var container = new PackFileContainer("Test"); - var newFiles = new List - { - new("Directory_0", new PackFile("file0.txt", null)), - }; - - container.AddFiles(newFiles); - container.AddFiles(newFiles); - - Assert.That(container.GetFileCount(), Is.EqualTo(1)); - } - - [Test] - public void AddFiles_WhiteSpaceInName() - { - var container = new PackFileContainer("Test"); - var newFiles = new List - { - new("Directory_0 ", new PackFile("file0.txt ", null)), - }; - - container.AddFiles(newFiles); - - Assert.That(container.GetFileCount(), Is.EqualTo(1)); - Assert.That(container.GetAllFiles().First().Key.Any(char.IsWhiteSpace), Is.False); - Assert.That(container.GetAllFiles().First().Value.Name.Any(char.IsWhiteSpace), Is.False); - } - - [Test] - public void AddFiles_EmptyFileName_Throws() - { - var container = new PackFileContainer("Test"); - var newFiles = new List - { - new("Directory_0", new PackFile("", null)), - }; - - Assert.Throws(() => container.AddFiles(newFiles)); - } - - [Test] - public void AddFiles_ReturnsAddedFiles() - { - var container = new PackFileContainer("Test"); - var file0 = new PackFile("file0.txt", null); - var file1 = new PackFile("file1.txt", null); - var newFiles = new List - { - new("Dir", file0), - new("Dir", file1), - }; - - var result = container.AddFiles(newFiles); - - Assert.That(result, Contains.Item(file0)); - Assert.That(result, Contains.Item(file1)); - } - } - - internal class PackFileContainer_DeleteFile - { - [Test] - public void DeleteFile_RemovesFile() - { - var container = CreateContainerWithFiles(); - var file = container.GetAllFiles().Values.First(); - - container.DeleteFile(file); - - Assert.That(container.GetFileCount(), Is.EqualTo(3)); - Assert.That(container.GetAllFiles().Values, Does.Not.Contain(file)); - } - - [Test] - public void DeleteFile_ReturnsDeletedFile() - { - var container = CreateContainerWithFiles(); - var file = container.GetAllFiles().Values.First(); - - var result = container.DeleteFile(file); - - Assert.That(result, Is.EqualTo(file)); - } - - static PackFileContainer CreateContainerWithFiles() - { - var container = new PackFileContainer("Test"); - container.AddFiles(new List - { - new("Dir_0", new PackFile("file0.txt", null)), - new("Dir_0", new PackFile("file1.txt", null)), - new("Dir_1", new PackFile("file0.txt", null)), - new("", new PackFile("rootFile.txt", null)), - }); - return container; - } - - [Test] - public void DeleteFile_FileNotInContainer_ReturnsNull() - { - var container = CreateContainerWithFiles(); - var orphanFile = new PackFile("orphan.txt", null); - - var result = container.DeleteFile(orphanFile); - - Assert.That(result, Is.Null); - Assert.That(container.GetFileCount(), Is.EqualTo(4)); - } - } - - internal class PackFileContainer_DeleteFolder - { - [Test] - public void DeleteFolder_RemovesFilesInFolder() - { - var container = CreateTestContainer(); - - container.DeleteFolder("directory_0"); - - Assert.That(container.GetFileCount(), Is.EqualTo(2)); - } - - [Test] - public void DeleteFolder_MissingFolder_NoEffect() - { - var container = CreateTestContainer(); - - container.DeleteFolder("nonexistent"); - - Assert.That(container.GetFileCount(), Is.EqualTo(8)); - } - - [Test] - public void DeleteFolder_WithSubFolder() - { - var container = CreateTestContainer(); - - container.DeleteFolder("directory_0\\subfolder"); - - Assert.That(container.GetFileCount(), Is.EqualTo(4)); - } - - [Test] - public void DeleteFolder_SimilarPrefixFolder_DoesNotDeleteWrongFiles() - { - var container = new PackFileContainer("Test"); - container.AddFiles(new List - { - new("dir", new PackFile("file0.txt", null)), - new("directory", new PackFile("file1.txt", null)), - }); - - container.DeleteFolder("dir"); - - Assert.That(container.GetFileCount(), Is.EqualTo(1)); - Assert.That(container.FindFile("directory\\file1.txt"), Is.Not.Null); - } - - static PackFileContainer CreateTestContainer() - { - var container = new PackFileContainer("Test"); - container.AddFiles(new List - { - new("Directory_0", new PackFile("file0.txt", null)), - new("Directory_0", new PackFile("file1.txt", null)), - new("Directory_0\\subfolder", new PackFile("subfile0.txt", null)), - new("Directory_0\\subfolder", new PackFile("subfile1.txt", null)), - new("Directory_0\\subfolder\\child", new PackFile("childFile0.txt", null)), - new("Directory_0\\subfolder\\child", new PackFile("childFile1.txt", null)), - new("Directory_1", new PackFile("file0.txt", null)), - new("", new PackFile("rootFile.txt", null)) - }); - Assert.That(container.GetFileCount(), Is.EqualTo(8)); - return container; - } - } - - internal class PackFileContainer_FindFile - { - [Test] - public void FindFile_ExistingFile() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("file0.txt", null); - container.AddFiles(new List { new("Dir", file) }); - - var result = container.FindFile("Dir\\file0.txt"); - - Assert.That(result, Is.EqualTo(file)); - } - - [Test] - public void FindFile_CaseInsensitive() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("file0.txt", null); - container.AddFiles(new List { new("Dir", file) }); - - var result = container.FindFile("DIR\\FILE0.TXT"); - - Assert.That(result, Is.EqualTo(file)); - } - - [Test] - public void FindFile_ForwardSlash() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("file0.txt", null); - container.AddFiles(new List { new("Dir", file) }); - - var result = container.FindFile("Dir/file0.txt"); - - Assert.That(result, Is.EqualTo(file)); - } - - [Test] - public void FindFile_NotFound_ReturnsNull() - { - var container = new PackFileContainer("Test"); - - var result = container.FindFile("nonexistent.txt"); - - Assert.That(result, Is.Null); - } - - [Test] - public void FindFile_TrimWhitespace() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("file0.txt", null); - container.AddFiles(new List { new("Dir", file) }); - - var result = container.FindFile(" Dir\\file0.txt "); - - Assert.That(result, Is.EqualTo(file)); - } - } - - internal class PackFileContainer_GetFullPath - { - [Test] - public void GetFullPath_ByReference() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("file0.txt", null); - container.AddFiles(new List { new("Dir", file) }); - - var result = container.GetFullPath(file); - - Assert.That(result, Is.EqualTo("dir\\file0.txt")); - } - - [Test] - public void GetFullPath_ByName() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("file0.txt", null); - container.AddFiles(new List { new("Dir", file) }); - - var otherRef = new PackFile("file0.txt", null); - var result = container.GetFullPath(otherRef); - - Assert.That(result, Is.EqualTo("dir\\file0.txt")); - } - - [Test] - public void GetFullPath_NotFound_ReturnsNull() - { - var container = new PackFileContainer("Test"); - - var result = container.GetFullPath(new PackFile("nonexistent.txt", null)); - - Assert.That(result, Is.Null); - } - } - - internal class PackFileContainer_MoveFile - { - [Test] - public void MoveFile_MovesToNewFolder() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("file0.txt", null); - container.AddFiles(new List { new("OldDir", file) }); - - container.MoveFile(file, "NewDir"); - - Assert.That(container.GetFileCount(), Is.EqualTo(1)); - Assert.That(container.ContainsFile("newdir\\file0.txt"), Is.True); - Assert.That(container.ContainsFile("olddir\\file0.txt"), Is.False); - } - - [Test] - public void MoveFile_NormalizesPathCasing() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("file0.txt", null); - container.AddFiles(new List { new("OldDir", file) }); - - container.MoveFile(file, "NewDir"); - - var found = container.FindFile("NewDir\\file0.txt"); - Assert.That(found, Is.Not.Null); - } - - } - - internal class PackFileContainer_RenameDirectory - { - [Test] - public void RenameDirectory_TopLevel() - { - var container = new PackFileContainer("Test"); - container.AddFiles(new List - { - new("OldName", new PackFile("file0.txt", null)), - new("OldName", new PackFile("file1.txt", null)), - }); - - var newNodePath = container.RenameDirectory("oldname", "NewName"); - - Assert.That(newNodePath, Is.EqualTo("NewName")); - Assert.That(container.GetFileCount(), Is.EqualTo(2)); - Assert.That(container.GetAllFiles().Keys.All(k => k.StartsWith("newname", StringComparison.OrdinalIgnoreCase)), Is.True); - } - - [Test] - public void RenameDirectory_Nested() - { - var container = new PackFileContainer("Test"); - container.AddFiles(new List - { - new("Parent\\OldChild", new PackFile("file0.txt", null)), - new("Parent\\OldChild\\Sub", new PackFile("file1.txt", null)), - }); - - var newNodePath = container.RenameDirectory("parent\\oldchild", "NewChild"); - - Assert.That(newNodePath, Is.EqualTo("parent\\NewChild")); - Assert.That(container.ContainsFile("parent\\newchild\\file0.txt"), Is.True); - Assert.That(container.ContainsFile("parent\\newchild\\sub\\file1.txt"), Is.True); - } - - [Test] - public void RenameDirectory_ReturnsNewNodePath() - { - var container = new PackFileContainer("Test"); - container.AddFiles(new List - { - new("Parent\\Child", new PackFile("file0.txt", null)), - }); - - var result = container.RenameDirectory("parent\\child", "RenamedChild"); - - Assert.That(result, Is.EqualTo("parent\\RenamedChild")); - } - - [Test] - public void RenameDirectory_NormalizesPathCasing() - { - var container = new PackFileContainer("Test"); - container.AddFiles(new List - { - new("OldDir", new PackFile("file.txt", null)), - }); - - container.RenameDirectory("olddir", "NewDir"); - - var found = container.FindFile("NewDir\\file.txt"); - Assert.That(found, Is.Not.Null); - } - } - - internal class PackFileContainer_RenameFile - { - [Test] - public void RenameFile_UpdatesNameAndPath() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("old.txt", null); - container.AddFiles(new List { new("Dir", file) }); - - container.RenameFile(file, "new.txt"); - - Assert.That(file.Name, Is.EqualTo("new.txt")); - Assert.That(container.GetFileCount(), Is.EqualTo(1)); - Assert.That(container.ContainsFile("dir\\new.txt"), Is.True); - } - - [Test] - public void RenameFile_RootFile_ProducesValidPath() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("old.txt", null); - container.AddFiles(new List { new("", file) }); - - container.RenameFile(file, "new.txt"); - - Assert.That(container.ContainsFile("new.txt"), Is.True); - Assert.That(container.GetAllFiles().Keys.Any(k => k.StartsWith("\\")), Is.False); - } - - [Test] - public void RenameFile_NormalizesPathCasing() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("old.txt", null); - container.AddFiles(new List { new("Dir", file) }); - - container.RenameFile(file, "New.txt"); - - var found = container.FindFile("Dir\\New.txt"); - Assert.That(found, Is.Not.Null); - } - - } - - internal class PackFileContainer_SaveFileData - { - [Test] - public void SaveFileData_UpdatesDataSource() - { - var container = new PackFileContainer("Test"); - var file = new PackFile("file.txt", new MemorySource([1, 2, 3])); - container.AddFiles(new List { new("Dir", file) }); - - var newData = new byte[] { 4, 5, 6, 7 }; - container.SaveFileData(file, newData); - - Assert.That(file.DataSource.ReadData(), Is.EqualTo(newData)); - } - - } - - internal class PackFileContainer_SearchFiles - { - private PackFileContainer _container; - - [SetUp] - public void Setup() - { - _container = new PackFileContainer("Test"); - _container.AddOrUpdateFile("models\\unit.rigid_model_v2", new PackFile("unit.rigid_model_v2", null)); - _container.AddOrUpdateFile("models\\vehicle.rigid_model_v2", new PackFile("vehicle.rigid_model_v2", null)); - _container.AddOrUpdateFile("models\\textures\\diffuse.dds", new PackFile("diffuse.dds", null)); - _container.AddOrUpdateFile("models\\textures\\normal.dds", new PackFile("normal.dds", null)); - _container.AddOrUpdateFile("audio\\battle_sound.wem", new PackFile("battle_sound.wem", null)); - _container.AddOrUpdateFile("audio\\music.wem", new PackFile("music.wem", null)); - _container.AddOrUpdateFile("scripts\\campaign_script.lua", new PackFile("campaign_script.lua", null)); - } - - [Test] - public void NullFilters_ReturnsAllFiles() - { - var results = _container.SearchFiles(null, null); - Assert.That(results.Count, Is.EqualTo(7)); - } - - [Test] - public void TextFilter_MatchesFileName() - { - var results = _container.SearchFiles("unit", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("unit.rigid_model_v2")); - } - - [Test] - public void TextFilter_IsCaseInsensitive() - { - var results = _container.SearchFiles("UNIT", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("unit.rigid_model_v2")); - } - - [Test] - public void TextFilter_PartialMatch() - { - var results = _container.SearchFiles("battle", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); - } - - [Test] - public void TextFilter_NoMatch_ReturnsEmpty() - { - var results = _container.SearchFiles("nonexistent", null); - Assert.That(results, Is.Empty); - } - - [Test] - public void ExtensionFilter_SingleExtension() - { - var results = _container.SearchFiles(null, [".wem"]); - Assert.That(results.Count, Is.EqualTo(2)); - Assert.That(results.All(r => r.File.Name.EndsWith(".wem")), Is.True); - } - - [Test] - public void ExtensionFilter_MultipleExtensions() - { - var results = _container.SearchFiles(null, [".wem", ".lua"]); - Assert.That(results.Count, Is.EqualTo(3)); - } - - [Test] - public void ExtensionFilter_NoMatch_ReturnsEmpty() - { - var results = _container.SearchFiles(null, [".xyz"]); - Assert.That(results, Is.Empty); - } - - [Test] - public void CombinedFilters_TextAndExtension() - { - var results = _container.SearchFiles("battle", [".wem"]); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); - } - - [Test] - public void CombinedFilters_TextMatchesButExtensionDoesNot_ReturnsEmpty() - { - var results = _container.SearchFiles("unit", [".wem"]); - Assert.That(results, Is.Empty); - } - - [Test] - public void ResultsAreSortedByPath() - { - var results = _container.SearchFiles(null, null); - var paths = results.Select(r => r.Path).ToList(); - var sorted = paths.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToList(); - Assert.That(paths, Is.EqualTo(sorted)); - } - - [Test] - public void ResultsContainCorrectPaths() - { - var results = _container.SearchFiles("diffuse", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].Path, Is.EqualTo("models\\textures\\diffuse.dds")); - } - - [Test] - public void EmptyTextFilter_TreatedAsNull() - { - var results = _container.SearchFiles(" ", null); - Assert.That(results.Count, Is.EqualTo(7)); - } - - [Test] - public void EmptyExtensionList_TreatedAsNoFilter() - { - var results = _container.SearchFiles(null, []); - Assert.That(results.Count, Is.EqualTo(7)); - } - } - - internal class PackFileContainer_GetDirectoryContent - { - [Test] - public void Root_ReturnsOnlyDirectFiles_Sorted() - { - var container = CreateContainer(); - - var entries = container.GetDirectoryContent(""); - var paths = entries.Select(x => x.Path).ToList(); - - Assert.That(paths, Is.EqualTo(new[] { "root_a.txt", "root_b.txt" })); - } - - [Test] - public void SubFolder_ReturnsOnlyDirectFiles() - { - var container = CreateContainer(); - - var entries = container.GetDirectoryContent("texture"); - - Assert.That(entries.Select(x => x.Path), Is.EqualTo(new[] { "texture\\texture_file.dds" })); - } - - [Test] - public void GetSubDirectories_ReturnsImmediateSubfolders() - { - var container = CreateContainer(); - - var subDirectories = container.GetSubDirectories("texture"); - - Assert.That(subDirectories, Is.EqualTo(new[] { "mesha", "meshb" })); - } - - [Test] - public void Utility_ComposesFilesAndSubfolders() - { - var container = CreateContainer(); - var split = PackFileServiceUtility.SplitDirectoryEntries(container, "texture"); - - Assert.That(split.Files.Select(x => x.FileName), Is.EqualTo(new[] { "texture_file.dds" })); - Assert.That(split.SubFolders, Is.EqualTo(new[] { "mesha", "meshb" })); - } - - private static PackFileContainer CreateContainer() - { - var container = new PackFileContainer("Test"); - var parent = new PackedFileSourceParent { FilePath = @"c:\game\p.pack" }; - - container.AddOrUpdateFile(@"audio\b.wem", new PackFile("b.wem", new PackedFileSource(parent, 0, 1, false, false, CompressionFormat.None, 0))); - container.AddOrUpdateFile(@"audio\a.wem", new PackFile("a.wem", new PackedFileSource(parent, 1, 1, false, false, CompressionFormat.None, 0))); - container.AddOrUpdateFile(@"texture\texture_file.dds", new PackFile("texture_file.dds", new PackedFileSource(parent, 2, 1, false, false, CompressionFormat.None, 0))); - container.AddOrUpdateFile(@"texture\mesha\filea", new PackFile("filea", new PackedFileSource(parent, 3, 1, false, false, CompressionFormat.None, 0))); - container.AddOrUpdateFile(@"texture\meshb\filea", new PackFile("filea", new PackedFileSource(parent, 4, 1, false, false, CompressionFormat.None, 0))); - container.AddOrUpdateFile(@"root_b.txt", new PackFile("root_b.txt", new PackedFileSource(parent, 5, 1, false, false, CompressionFormat.None, 0))); - container.AddOrUpdateFile(@"root_a.txt", new PackFile("root_a.txt", new PackedFileSource(parent, 6, 1, false, false, CompressionFormat.None, 0))); - - return container; - } - } -} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_AddFiles.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_AddFiles.cs new file mode 100644 index 000000000..9a2daeb49 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_AddFiles.cs @@ -0,0 +1,48 @@ +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_AddFiles : PackFileContainerTests_TestBase + { + public PackFileContainerTests_AddFiles(Type containerType) : base(containerType) { } + + [Test] + public void AddFiles_AddsMultipleFilesOrThrowsOnCached() + { + var newFiles = new List + { + new("dir", new PackFile("a.txt", null)), + new("", new PackFile("root.txt", null)) + }; + + if (IsCachedContainer) + { + Assert.Throws(() => _container.AddFiles(newFiles)); + return; + } + + var added = _container.AddFiles(newFiles); + Assert.That(added.Count, Is.EqualTo(2)); + Assert.That(_container.ContainsFile("dir\\a.txt"), Is.True); + Assert.That(_container.ContainsFile("root.txt"), Is.True); + } + + [Test] + public void AddFiles_EmptyFileName_Throws() + { + if (IsCachedContainer) + { + Assert.Throws(() => + _container.AddFiles([new NewPackFileEntry("dir", new PackFile("", null))])); + return; + } + + Assert.Throws(() => + _container.AddFiles([new NewPackFileEntry("dir", new PackFile("", null))])); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_AddOrUpdateFile.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_AddOrUpdateFile.cs new file mode 100644 index 000000000..83e3d5950 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_AddOrUpdateFile.cs @@ -0,0 +1,41 @@ +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_AddOrUpdateFile : PackFileContainerTests_TestBase + { + public PackFileContainerTests_AddOrUpdateFile(Type containerType) : base(containerType) { } + + [Test] + public void AddOrUpdateFile_NewPath_AddsFileOrThrowsOnCached() + { + if (IsCachedContainer) + { + Assert.Throws(() => + _container.AddOrUpdateFile("new\\file.txt", new PackFile("file.txt", null))); + return; + } + + _container.AddOrUpdateFile("new\\file.txt", new PackFile("file.txt", null)); + Assert.That(_container.ContainsFile("new\\file.txt"), Is.True); + } + + [Test] + public void AddOrUpdateFile_ExistingPath_UpdatesWithoutIncreasingCount() + { + if (IsCachedContainer) + { + Assert.Throws(() => + _container.AddOrUpdateFile("folder\\file.txt", new PackFile("file.txt", null))); + return; + } + + var before = _container.GetFileCount(); + _container.AddOrUpdateFile("folder\\file.txt", new PackFile("file.txt", null)); + Assert.That(_container.GetFileCount(), Is.EqualTo(before)); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ContainsFile.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ContainsFile.cs new file mode 100644 index 000000000..fafa504c4 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ContainsFile.cs @@ -0,0 +1,23 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_ContainsFile : PackFileContainerTests_TestBase + { + public PackFileContainerTests_ContainsFile(Type containerType) : base(containerType) { } + + [Test] + public void ContainsFile_TrueForNormalizedInput() + { + Assert.That(_container.ContainsFile(" folder/file.txt "), Is.True); + } + + [Test] + public void ContainsFile_FalseForMissing() + { + Assert.That(_container.ContainsFile("missing.txt"), Is.False); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_DeleteFile.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_DeleteFile.cs new file mode 100644 index 000000000..6bfd379fa --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_DeleteFile.cs @@ -0,0 +1,41 @@ +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_DeleteFile : PackFileContainerTests_TestBase + { + public PackFileContainerTests_DeleteFile(Type containerType) : base(containerType) { } + + [Test] + public void DeleteFile_RemovesFileOrThrowsOnCached() + { + var file = _container.FindFile("folder\\file.txt")!; + + if (IsCachedContainer) + { + Assert.Throws(() => _container.DeleteFile(file)); + return; + } + + var deleted = _container.DeleteFile(file); + Assert.That(deleted, Is.EqualTo(file)); + Assert.That(_container.ContainsFile("folder\\file.txt"), Is.False); + } + + [Test] + public void DeleteFile_UnknownFile_ReturnsNullForWritableContainer() + { + if (IsCachedContainer) + { + var known = _container.FindFile("folder\\file.txt")!; + Assert.Throws(() => _container.DeleteFile(known)); + return; + } + + Assert.That(_container.DeleteFile(new PackFile("x.txt", null)), Is.Null); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_DeleteFolder.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_DeleteFolder.cs new file mode 100644 index 000000000..5ee657f27 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_DeleteFolder.cs @@ -0,0 +1,25 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_DeleteFolder : PackFileContainerTests_TestBase + { + public PackFileContainerTests_DeleteFolder(Type containerType) : base(containerType) { } + + [Test] + public void DeleteFolder_RemovesFolderTreeOrThrowsOnCached() + { + if (IsCachedContainer) + { + Assert.Throws(() => _container.DeleteFolder("models")); + return; + } + + _container.DeleteFolder("models"); + Assert.That(_container.ContainsFile("models\\unit.model"), Is.False); + Assert.That(_container.ContainsFile("models\\textures\\diffuse.dds"), Is.False); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_EdgeCases.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_EdgeCases.cs deleted file mode 100644 index e9380a279..000000000 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_EdgeCases.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Shared.Core.PackFiles; -using Shared.Core.PackFiles.Models.Containers; -using Shared.Core.PackFiles.Models.FileSources; -using Shared.Core.PackFiles.Utility; - -namespace Shared.CoreTest.PackFiles.Models.Containers -{ - [TestFixture(typeof(CachedPackFileContainer))] - [TestFixture(typeof(PackFileContainer))] - internal class PackFileContainerTests_EdgeCases : PackFileContainerTests_TestBase - { - public PackFileContainerTests_EdgeCases(Type containerType) - : base(containerType) - { - } - - [Test] - public void FindAllWithExtention_IsCaseInsensitive() - { - var upper = _container.FindAllWithExtention(".WEM"); - var lower = _container.FindAllWithExtention(".wem"); - Assert.That(upper.Count, Is.EqualTo(3)); - Assert.That(lower.Count, Is.EqualTo(3)); - } - - [Test] - public void ContainsFile_NormalizesForwardSlashes() - { - Assert.That(_container.ContainsFile("folder/file.txt"), Is.True); - Assert.That(_container.ContainsFile("FOLDER/FILE.TXT"), Is.True); - Assert.That(_container.ContainsFile(" folder\\file.txt "), Is.True); - } - - [Test] - public void GetAllFiles_PreservesCompressionMetadata() - { - var all = _container.GetAllFiles(); - var source = (PackedFileSource)all["compressed\\data.bin"].DataSource; - - Assert.That(source.IsEncrypted, Is.True); - Assert.That(source.IsCompressed, Is.True); - Assert.That(source.CompressionFormat, Is.EqualTo(CompressionFormat.Lz4)); - Assert.That(source.UncompressedSize, Is.EqualTo(2000)); - Assert.That(source.Offset, Is.EqualTo(1000)); - Assert.That(source.Size, Is.EqualTo(500)); - } - - [Test] - public void GetFullPath_WithDuplicateFileNames_ReturnsFirstMatch() - { - var file = _container.FindFile("folder_a\\shared.txt")!; - var path = _container.GetFullPath(file); - - Assert.That(path, Does.Contain("shared.txt")); - Assert.That(path, Does.Contain("\\")); - } - - [Test] - public void GetDirectoryContent_UnknownFolder_ReturnsEmpty() - { - Assert.That(_container.GetDirectoryContent("missing\\folder"), Is.Empty); - Assert.That(_container.GetSubDirectories("missing\\folder"), Is.Empty); - } - } -} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_FindAllWithExtention.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_FindAllWithExtention.cs new file mode 100644 index 000000000..4273c082a --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_FindAllWithExtention.cs @@ -0,0 +1,24 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_FindAllWithExtention : PackFileContainerTests_TestBase + { + public PackFileContainerTests_FindAllWithExtention(Type containerType) : base(containerType) { } + + [Test] + public void FindAllWithExtention_IsCaseInsensitive() + { + Assert.That(_container.FindAllWithExtention(".wem").Count, Is.EqualTo(3)); + Assert.That(_container.FindAllWithExtention(".WEM").Count, Is.EqualTo(3)); + } + + [Test] + public void FindAllWithExtention_NoMatch_ReturnsEmpty() + { + Assert.That(_container.FindAllWithExtention(".xyz"), Is.Empty); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_FindFile.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_FindFile.cs new file mode 100644 index 000000000..05fbefc7b --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_FindFile.cs @@ -0,0 +1,32 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_FindFile : PackFileContainerTests_TestBase + { + public PackFileContainerTests_FindFile(Type containerType) : base(containerType) { } + + [Test] + public void FindFile_FindsExistingPath() + { + var file = _container.FindFile("folder\\file.txt"); + Assert.That(file, Is.Not.Null); + Assert.That(file!.Name, Is.EqualTo("file.txt")); + } + + [Test] + public void FindFile_NormalizesCaseAndSlashes() + { + var file = _container.FindFile("FOLDER/FILE.TXT"); + Assert.That(file, Is.Not.Null); + } + + [Test] + public void FindFile_MissingPath_ReturnsNull() + { + Assert.That(_container.FindFile("missing\\file.txt"), Is.Null); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetAllFiles.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetAllFiles.cs new file mode 100644 index 000000000..69b345954 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetAllFiles.cs @@ -0,0 +1,30 @@ +using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.PackFiles.Utility; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_GetAllFiles : PackFileContainerTests_TestBase + { + public PackFileContainerTests_GetAllFiles(Type containerType) : base(containerType) { } + + [Test] + public void GetAllFiles_ReturnsExpectedCountAndKeys() + { + var files = _container.GetAllFiles(); + Assert.That(files.Count, Is.EqualTo(15)); + Assert.That(files.ContainsKey("folder\\file.txt"), Is.True); + Assert.That(files.ContainsKey("scripts\\campaign_script.lua"), Is.True); + } + + [Test] + public void GetAllFiles_PreservesCompressionMetadata() + { + var source = (PackedFileSource)_container.GetAllFiles()["compressed\\data.bin"].DataSource; + Assert.That(source.CompressionFormat, Is.EqualTo(CompressionFormat.Lz4)); + Assert.That(source.UncompressedSize, Is.EqualTo(2000)); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent.cs index 35427ddf1..8a2ccb56a 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent.cs @@ -1,5 +1,4 @@ using Shared.Core.PackFiles.Models.Containers; -using Shared.Core.PackFiles.Models.FileSources; using Shared.Core.PackFiles.Utility; namespace Shared.CoreTest.PackFiles.Models.Containers @@ -8,105 +7,39 @@ namespace Shared.CoreTest.PackFiles.Models.Containers [TestFixture(typeof(PackFileContainer))] internal class PackFileContainerTests_GetDirectoryContent : PackFileContainerTests_TestBase { - public PackFileContainerTests_GetDirectoryContent(Type containerType) - : base(containerType) - { - } - - [Test] - public void Root_ReturnsRootFilesAndTopFolders() - { - var content = PackFileServiceUtility.SplitDirectoryEntries(_container, ""); - - Assert.That(content.Files.Any(f => f.FileName == "root_file.txt"), Is.True); - Assert.That(content.SubFolders, Does.Contain("models")); - Assert.That(content.SubFolders, Does.Contain("audio")); - Assert.That(content.SubFolders, Does.Not.Contain("textures")); - } - - [Test] - public void Root_DoesNotIncludeFilesFromSubfolders() - { - var entries = _container.GetDirectoryContent(""); - - Assert.That(entries.Count, Is.EqualTo(1)); - Assert.That(entries[0].File.Name, Is.EqualTo("root_file.txt")); - } + public PackFileContainerTests_GetDirectoryContent(Type containerType) : base(containerType) { } [Test] - public void Subfolder_ReturnsDirectFilesAndImmediateSubfolders() + public void GetDirectoryContent_Root_ReturnsDirectFilesOnly() { - var entries = _container.GetDirectoryContent("models"); - var subFolders = _container.GetSubDirectories("models"); - - Assert.That(entries.Any(f => f.File.Name == "unit.model"), Is.True); - Assert.That(entries.Any(f => f.File.Name == "vehicle.model"), Is.True); - Assert.That(subFolders, Does.Contain("textures")); - Assert.That(entries.Any(f => f.File.Name == "diffuse.dds"), Is.False); + var rows = _container.GetDirectoryContent(""); + Assert.That(rows.Count, Is.EqualTo(1)); + Assert.That(rows[0].Path, Is.EqualTo("root_file.txt")); } [Test] - public void NestedSubfolder_ReturnsCorrectContent() + public void GetDirectoryContent_NestedFolder_ReturnsOnlyItsFiles() { - var entries = _container.GetDirectoryContent("models\\textures"); - var subFolders = _container.GetSubDirectories("models\\textures"); - - Assert.That(entries.Any(f => f.File.Name == "diffuse.dds"), Is.True); - Assert.That(entries.Any(f => f.File.Name == "normal.dds"), Is.True); - Assert.That(subFolders, Does.Contain("specular")); - Assert.That(entries.Count, Is.EqualTo(2)); + var rows = _container.GetDirectoryContent(@"models\textures"); + Assert.That(rows.Select(x => x.Path), Is.EquivalentTo(new[] + { + @"models\textures\diffuse.dds", + @"models\textures\normal.dds" + })); } [Test] - public void DeepNestedFolder_ReturnsOnlyItsFiles() + public void GetDirectoryContent_UnknownFolder_ReturnsEmpty() { - var entries = _container.GetDirectoryContent("models\\textures\\specular"); - var subFolders = _container.GetSubDirectories("models\\textures\\specular"); - - Assert.That(entries.Count, Is.EqualTo(1)); - Assert.That(entries[0].File.Name, Is.EqualTo("gloss.dds")); - Assert.That(subFolders, Is.Empty); + Assert.That(_container.GetDirectoryContent("missing\\folder"), Is.Empty); } [Test] - public void NonexistentFolder_ReturnsEmpty() + public void GetDirectoryContent_ComposesWithUtility() { - var entries = _container.GetDirectoryContent("nonexistent\\path"); - var subFolders = _container.GetSubDirectories("nonexistent\\path"); - - Assert.That(entries, Is.Empty); - Assert.That(subFolders, Is.Empty); - } - - [Test] - public void SubfoldersAreSorted() - { - var subFolders = _container.GetSubDirectories(""); - - var sorted = subFolders.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList(); - Assert.That(subFolders, Is.EqualTo(sorted)); - } - - [Test] - public void FilesAreSorted() - { - var entries = _container.GetDirectoryContent("models"); - - var sorted = entries.OrderBy(x => x.File.Name, StringComparer.CurrentCultureIgnoreCase).ToList(); - Assert.That(entries.Select(f => f.File.Name), Is.EqualTo(sorted.Select(f => f.File.Name))); - } - - [Test] - public void Files_HaveCorrectDataSource() - { - var entries = _container.GetDirectoryContent("models"); - var unitFile = entries.First(f => f.File.Name == "unit.model"); - var source = unitFile.File.DataSource as PackedFileSource; - - Assert.That(source, Is.Not.Null); - Assert.That(source.Offset, Is.EqualTo(10)); - Assert.That(source.Size, Is.EqualTo(20)); - Assert.That(source.Parent.FilePath, Is.EqualTo(@"c:\game\data\pack1.pack")); + var split = PackFileServiceUtility.SplitDirectoryEntries(_container, "models"); + Assert.That(split.Files.Any(x => x.FileName == "unit.model"), Is.True); + Assert.That(split.SubFolders, Does.Contain("textures")); } } } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent_PathRows.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent_PathRows.cs deleted file mode 100644 index 799a8caf2..000000000 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetDirectoryContent_PathRows.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Shared.Core.PackFiles.Models.Containers; - -namespace Shared.CoreTest.PackFiles.Models.Containers -{ - [TestFixture(typeof(CachedPackFileContainer))] - [TestFixture(typeof(PackFileContainer))] - internal class PackFileContainerTests_GetDirectoryContent_PathRows : PackFileContainerTests_TestBase - { - public PackFileContainerTests_GetDirectoryContent_PathRows(Type containerType) - : base(containerType) - { - } - - [Test] - public void Root_IncludesOnlyRootFiles() - { - var rows = _container.GetDirectoryContent(""); - var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - - Assert.That(paths, Does.Contain("root_file.txt")); - } - - [Test] - public void Root_ExcludesDeepDescendantFolders() - { - var rows = _container.GetDirectoryContent(""); - var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - - Assert.That(paths, Does.Not.Contain(@"texture\mesha\filea")); - Assert.That(paths, Does.Not.Contain(@"texture\meshb\filea")); - } - - [Test] - public void Root_ReturnsExpectedRowCount() - { - var rows = _container.GetDirectoryContent(""); - Assert.That(rows.Count, Is.EqualTo(1)); - } - - [Test] - public void Subfolder_IncludesOnlyDirectFiles() - { - var rows = _container.GetDirectoryContent(@"models\textures"); - var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - - Assert.That(paths, Does.Contain(@"models\textures\diffuse.dds")); - Assert.That(paths, Does.Contain(@"models\textures\normal.dds")); - Assert.That(paths.Count, Is.EqualTo(2)); - } - - [Test] - public void Subfolder_ExcludesParentAndSiblingRows() - { - var rows = _container.GetDirectoryContent(@"models\textures"); - var paths = rows.Select(r => r.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - - Assert.That(paths.Any(p => p.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.False); - Assert.That(paths.Any(p => !p.StartsWith(@"models\textures\", StringComparison.OrdinalIgnoreCase)), Is.False); - } - - [Test] - public void Subfolder_ReturnsExpectedRowCount() - { - var rows = _container.GetDirectoryContent(@"models\textures"); - Assert.That(rows.Count, Is.EqualTo(2)); - } - - [Test] - public void LeafFolder_ReturnsOnlyDirectFiles() - { - var rows = _container.GetDirectoryContent(@"audio"); - Assert.That(rows.Count, Is.EqualTo(3)); - Assert.That(rows.All(r => r.Path.StartsWith(@"audio\", StringComparison.OrdinalIgnoreCase)), Is.True); - } - } -} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFileCount.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFileCount.cs new file mode 100644 index 000000000..8b5e2355b --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFileCount.cs @@ -0,0 +1,17 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_GetFileCount : PackFileContainerTests_TestBase + { + public PackFileContainerTests_GetFileCount(Type containerType) : base(containerType) { } + + [Test] + public void GetFileCount_ReturnsMasterDatasetCount() + { + Assert.That(_container.GetFileCount(), Is.EqualTo(15)); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFullPath.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFullPath.cs new file mode 100644 index 000000000..e7f17ab95 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFullPath.cs @@ -0,0 +1,27 @@ +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_GetFullPath : PackFileContainerTests_TestBase + { + public PackFileContainerTests_GetFullPath(Type containerType) : base(containerType) { } + + [Test] + public void GetFullPath_ReturnsStoredPath() + { + var file = _container.FindFile("folder\\file.txt")!; + var path = _container.GetFullPath(file); + Assert.That(path, Is.EqualTo("folder\\file.txt")); + } + + [Test] + public void GetFullPath_UnknownFile_ReturnsNull() + { + var unknown = new PackFile("none.txt", null); + Assert.That(_container.GetFullPath(unknown), Is.Null); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetSubDirectories.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetSubDirectories.cs new file mode 100644 index 000000000..2aa8aa55c --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetSubDirectories.cs @@ -0,0 +1,35 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_GetSubDirectories : PackFileContainerTests_TestBase + { + public PackFileContainerTests_GetSubDirectories(Type containerType) : base(containerType) { } + + [Test] + public void GetSubDirectories_Root_ReturnsImmediateSortedFolders() + { + var folders = _container.GetSubDirectories(""); + Assert.That(folders, Does.Contain("audio")); + Assert.That(folders, Does.Contain("models")); + + var sorted = folders.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList(); + Assert.That(folders, Is.EqualTo(sorted)); + } + + [Test] + public void GetSubDirectories_NestedFolder_ReturnsOnlyChildren() + { + var folders = _container.GetSubDirectories(@"models\textures"); + Assert.That(folders, Is.EqualTo(new[] { "specular" })); + } + + [Test] + public void GetSubDirectories_UnknownFolder_ReturnsEmpty() + { + Assert.That(_container.GetSubDirectories("missing\\folder"), Is.Empty); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_IsCaPackFile.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_IsCaPackFile.cs new file mode 100644 index 000000000..48627d47a --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_IsCaPackFile.cs @@ -0,0 +1,24 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_IsCaPackFile : PackFileContainerTests_TestBase + { + public PackFileContainerTests_IsCaPackFile(Type containerType) : base(containerType) { } + + [Test] + public void IsCaPackFile_AlwaysStartsTrue() + { + Assert.That(_container.IsCaPackFile, Is.True); + } + + [Test] + public void IsCaPackFile_SetterBehavior_MatchesContainerType() + { + _container.IsCaPackFile = false; + Assert.That(_container.IsCaPackFile, Is.EqualTo(IsCachedContainer)); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_MoveFile.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_MoveFile.cs new file mode 100644 index 000000000..a55191d0f --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_MoveFile.cs @@ -0,0 +1,27 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_MoveFile : PackFileContainerTests_TestBase + { + public PackFileContainerTests_MoveFile(Type containerType) : base(containerType) { } + + [Test] + public void MoveFile_MovesFileOrThrowsOnCached() + { + var file = _container.FindFile("folder\\file.txt")!; + + if (IsCachedContainer) + { + Assert.Throws(() => _container.MoveFile(file, "other")); + return; + } + + _container.MoveFile(file, "other"); + Assert.That(_container.ContainsFile("other\\file.txt"), Is.True); + Assert.That(_container.ContainsFile("folder\\file.txt"), Is.False); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_Name.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_Name.cs new file mode 100644 index 000000000..9e158ff34 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_Name.cs @@ -0,0 +1,17 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_Name : PackFileContainerTests_TestBase + { + public PackFileContainerTests_Name(Type containerType) : base(containerType) { } + + [Test] + public void Name_IsSetFromContainerCreation() + { + Assert.That(_container.Name, Is.EqualTo("TestCache")); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ReadOnly.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ReadOnly.cs deleted file mode 100644 index c3b5dfbca..000000000 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_ReadOnly.cs +++ /dev/null @@ -1,198 +0,0 @@ -using Shared.Core.PackFiles; -using Shared.Core.PackFiles.Models; -using Shared.Core.PackFiles.Models.Containers; -using Shared.Core.PackFiles.Models.FileSources; -using Shared.Core.Settings; - -namespace Shared.CoreTest.PackFiles.Models.Containers -{ - [TestFixture(typeof(CachedPackFileContainer))] - [TestFixture(typeof(PackFileContainer))] - internal class PackFileContainerTests_ReadOnly : PackFileContainerTests_TestBase - { - public PackFileContainerTests_ReadOnly(Type containerType) - : base(containerType) - { - } - - [Test] - public void IsCaPackFile_AlwaysTrue() - { - Assert.That(_container.IsCaPackFile, Is.True); - } - - [Test] - public void IsCaPackFile_SetterDoesNotChangeValue() - { - _container.IsCaPackFile = false; - if (IsCachedContainer) - Assert.That(_container.IsCaPackFile, Is.True); - else - Assert.That(_container.IsCaPackFile, Is.False); - } - - [Test] - public void GetFileCount_ReturnsCorrectCount() - { - Assert.That(_container.GetFileCount(), Is.EqualTo(15)); - } - - [Test] - public void FindFile_ReturnsFile() - { - var result = _container.FindFile("folder\\file.txt"); - Assert.That(result, Is.Not.Null); - Assert.That(result.Name, Is.EqualTo("file.txt")); - } - - [Test] - public void FindFile_ReturnsNullForMissing() - { - var result = _container.FindFile("missing\\path.txt"); - Assert.That(result, Is.Null); - } - - [Test] - public void FindFile_NormalizesPath() - { - var result = _container.FindFile("FOLDER/FILE.TXT"); - Assert.That(result, Is.Not.Null); - } - - [Test] - public void FindFile_ReturnsCorrectDataSource() - { - var result = _container.FindFile("folder\\file.txt"); - var source = result!.DataSource as PackedFileSource; - Assert.That(source, Is.Not.Null); - Assert.That(source.Offset, Is.EqualTo(100)); - Assert.That(source.Size, Is.EqualTo(200)); - } - - [Test] - public void ContainsFile_ReturnsTrueForExisting() - { - Assert.That(_container.ContainsFile("folder\\file.txt"), Is.True); - } - - [Test] - public void ContainsFile_ReturnsFalseForMissing() - { - Assert.That(_container.ContainsFile("missing.txt"), Is.False); - } - - [Test] - public void GetFullPath_ReturnsPath() - { - var file = _container.FindFile("folder\\file.txt")!; - var path = _container.GetFullPath(file); - Assert.That(path, Is.EqualTo("folder\\file.txt")); - } - - [Test] - public void GetFullPath_ReturnsNullForMissing() - { - var unknownFile = new PackFile("unknown.txt", null); - var path = _container.GetFullPath(unknownFile); - Assert.That(path, Is.Null); - } - - [Test] - public void FindAllWithExtention_ReturnsMatching() - { - var results = _container.FindAllWithExtention(".wem"); - Assert.That(results.Count, Is.EqualTo(3)); - Assert.That(results[0].FileName, Does.EndWith("sound.wem")); - } - - [Test] - public void FindAllWithExtention_ReturnsEmptyForNoMatch() - { - var results = _container.FindAllWithExtention(".xyz"); - Assert.That(results, Is.Empty); - } - - [Test] - public void GetAllFiles_ReturnsAllFiles() - { - var all = _container.GetAllFiles(); - Assert.That(all.Count, Is.EqualTo(15)); - Assert.That(all.ContainsKey("folder\\file.txt"), Is.True); - Assert.That(all.ContainsKey("other\\data.bin"), Is.True); - Assert.That(all.ContainsKey("audio\\sound.wem"), Is.True); - } - - [Test] - public void AddFiles_Throws() - { - var cachedContainer = GetCachedContainerOrIgnore(nameof(AddFiles_Throws)); - var newFiles = new List - { - new("folder", new PackFile("new.txt", null)) - }; - Assert.Throws(() => cachedContainer.AddFiles(newFiles)); - } - - [Test] - public void DeleteFile_Throws() - { - var cachedContainer = GetCachedContainerOrIgnore(nameof(DeleteFile_Throws)); - var file = _container.FindFile("folder\\file.txt")!; - Assert.Throws(() => cachedContainer.DeleteFile(file)); - } - - [Test] - public void DeleteFolder_Throws() - { - var cachedContainer = GetCachedContainerOrIgnore(nameof(DeleteFolder_Throws)); - Assert.Throws(() => cachedContainer.DeleteFolder("folder")); - } - - [Test] - public void MoveFile_Throws() - { - var cachedContainer = GetCachedContainerOrIgnore(nameof(MoveFile_Throws)); - var file = _container.FindFile("folder\\file.txt")!; - Assert.Throws(() => cachedContainer.MoveFile(file, "other")); - } - - [Test] - public void RenameDirectory_Throws() - { - var cachedContainer = GetCachedContainerOrIgnore(nameof(RenameDirectory_Throws)); - Assert.Throws(() => cachedContainer.RenameDirectory("folder", "renamed")); - } - - [Test] - public void RenameFile_Throws() - { - var cachedContainer = GetCachedContainerOrIgnore(nameof(RenameFile_Throws)); - var file = _container.FindFile("folder\\file.txt")!; - Assert.Throws(() => cachedContainer.RenameFile(file, "renamed.txt")); - } - - [Test] - public void SaveFileData_Throws() - { - var cachedContainer = GetCachedContainerOrIgnore(nameof(SaveFileData_Throws)); - var file = _container.FindFile("folder\\file.txt")!; - Assert.Throws(() => cachedContainer.SaveFileData(file, [1, 2, 3])); - } - - [Test] - public void SaveToDisk_Throws() - { - var cachedContainer = GetCachedContainerOrIgnore(nameof(SaveToDisk_Throws)); - var gameInfo = GameInformationDatabase.GetGameById(GameTypeEnum.Warhammer3); - Assert.Throws(() => cachedContainer.SaveToDisk("path", false, gameInfo)); - } - - [Test] - public void AddOrUpdateFile_Throws() - { - IgnoreIfNotCached(nameof(AddOrUpdateFile_Throws)); - Assert.Throws(() => - _container.AddOrUpdateFile("test\\new.txt", new PackFile("new.txt", null))); - } - } -} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_RenameDirectory.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_RenameDirectory.cs new file mode 100644 index 000000000..0852a193f --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_RenameDirectory.cs @@ -0,0 +1,25 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_RenameDirectory : PackFileContainerTests_TestBase + { + public PackFileContainerTests_RenameDirectory(Type containerType) : base(containerType) { } + + [Test] + public void RenameDirectory_RenamesNodeOrThrowsOnCached() + { + if (IsCachedContainer) + { + Assert.Throws(() => _container.RenameDirectory("models", "newmodels")); + return; + } + + var newPath = _container.RenameDirectory("models", "newmodels"); + Assert.That(newPath, Is.EqualTo("newmodels")); + Assert.That(_container.ContainsFile("newmodels\\unit.model"), Is.True); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_RenameFile.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_RenameFile.cs new file mode 100644 index 000000000..dadfa08c4 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_RenameFile.cs @@ -0,0 +1,27 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_RenameFile : PackFileContainerTests_TestBase + { + public PackFileContainerTests_RenameFile(Type containerType) : base(containerType) { } + + [Test] + public void RenameFile_RenamesFileOrThrowsOnCached() + { + var file = _container.FindFile("folder\\file.txt")!; + + if (IsCachedContainer) + { + Assert.Throws(() => _container.RenameFile(file, "renamed.txt")); + return; + } + + _container.RenameFile(file, "renamed.txt"); + Assert.That(_container.ContainsFile("folder\\renamed.txt"), Is.True); + Assert.That(_container.ContainsFile("folder\\file.txt"), Is.False); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SaveFileData.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SaveFileData.cs new file mode 100644 index 000000000..6673b0dce --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SaveFileData.cs @@ -0,0 +1,30 @@ +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.FileSources; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_SaveFileData : PackFileContainerTests_TestBase + { + public PackFileContainerTests_SaveFileData(Type containerType) : base(containerType) { } + + [Test] + public void SaveFileData_UpdatesMemorySourceOrThrowsOnCached() + { + if (IsCachedContainer) + { + var cachedFile = _container.FindFile("folder\\file.txt")!; + Assert.Throws(() => _container.SaveFileData(cachedFile, [1, 2, 3])); + return; + } + + var file = new PackFile("new.txt", new MemorySource([1])); + _container.AddOrUpdateFile("new.txt", file); + _container.SaveFileData(file, [9, 8, 7]); + + Assert.That(file.DataSource.ReadData(), Is.EqualTo(new byte[] { 9, 8, 7 })); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SaveToDisk.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SaveToDisk.cs new file mode 100644 index 000000000..332a2a23c --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SaveToDisk.cs @@ -0,0 +1,22 @@ +using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.Settings; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_SaveToDisk : PackFileContainerTests_TestBase + { + public PackFileContainerTests_SaveToDisk(Type containerType) : base(containerType) { } + + [Test] + public void SaveToDisk_CachedContainer_Throws() + { + if (!IsCachedContainer) + Assert.Ignore("Writable pack SaveToDisk behavior is validated in serializer/integration tests."); + + var gameInfo = GameInformationDatabase.GetGameById(GameTypeEnum.Warhammer3); + Assert.Throws(() => _container.SaveToDisk("path", false, gameInfo)); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs index 7e5c3cdd8..8e67460a8 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs @@ -6,105 +6,33 @@ namespace Shared.CoreTest.PackFiles.Models.Containers [TestFixture(typeof(PackFileContainer))] internal class PackFileContainerTests_SearchFiles : PackFileContainerTests_TestBase { - public PackFileContainerTests_SearchFiles(Type containerType) - : base(containerType) - { - } + public PackFileContainerTests_SearchFiles(Type containerType) : base(containerType) { } [Test] - public void NullFilters_ReturnsAllFiles() + public void SearchFiles_NullFilters_ReturnsAll() { var results = _container.SearchFiles(null, null); Assert.That(results.Count, Is.EqualTo(15)); } [Test] - public void TextFilter_MatchesFileName() - { - var results = _container.SearchFiles("unit", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("unit.model")); - } - - [Test] - public void TextFilter_IsCaseInsensitive() - { - var results = _container.SearchFiles("UNIT", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("unit.model")); - } - - [Test] - public void TextFilter_PartialMatch() - { - var results = _container.SearchFiles("battle", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); - } - - [Test] - public void TextFilter_NoMatch_ReturnsEmpty() - { - var results = _container.SearchFiles("nonexistent", null); - Assert.That(results, Is.Empty); - } - - [Test] - public void ExtensionFilter_SingleExtension() - { - var results = _container.SearchFiles(null, [".wem"]); - Assert.That(results.Count, Is.EqualTo(3)); - Assert.That(results.All(r => r.File.Name.EndsWith(".wem")), Is.True); - } - - [Test] - public void ExtensionFilter_MultipleExtensions() - { - var results = _container.SearchFiles(null, [".wem", ".lua"]); - Assert.That(results.Count, Is.EqualTo(4)); - } - - [Test] - public void ExtensionFilter_NoMatch_ReturnsEmpty() - { - var results = _container.SearchFiles(null, [".xyz"]); - Assert.That(results, Is.Empty); - } - - [Test] - public void CombinedFilters_TextAndExtension() + public void SearchFiles_TextAndExtension_FiltersCorrectly() { var results = _container.SearchFiles("battle", [".wem"]); Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].File.Name, Is.EqualTo("battle_sound.wem")); + Assert.That(results[0].Path, Is.EqualTo(@"audio\battle_sound.wem")); } [Test] - public void CombinedFilters_TextMatchesButExtensionDoesNot_ReturnsEmpty() + public void SearchFiles_ResultsArePathSorted() { - var results = _container.SearchFiles("unit", [".wem"]); - Assert.That(results, Is.Empty); - } - - [Test] - public void ResultsAreSortedByPath() - { - var results = _container.SearchFiles(null, null); - var paths = results.Select(r => r.Path).ToList(); - var sorted = paths.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).ToList(); + var paths = _container.SearchFiles(null, null).Select(x => x.Path).ToList(); + var sorted = paths.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); Assert.That(paths, Is.EqualTo(sorted)); } [Test] - public void ResultsContainCorrectPaths() - { - var results = _container.SearchFiles("diffuse", null); - Assert.That(results.Count, Is.EqualTo(1)); - Assert.That(results[0].Path, Is.EqualTo("models\\textures\\diffuse.dds")); - } - - [Test] - public void EmptyExtensionList_TreatedAsNoFilter() + public void SearchFiles_EmptyExtensionList_TreatedAsNoFilter() { var results = _container.SearchFiles(null, []); Assert.That(results.Count, Is.EqualTo(15)); diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SystemFilePath.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SystemFilePath.cs new file mode 100644 index 000000000..9a2a83c28 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SystemFilePath.cs @@ -0,0 +1,17 @@ +using Shared.Core.PackFiles.Models.Containers; + +namespace Shared.CoreTest.PackFiles.Models.Containers +{ + [TestFixture(typeof(CachedPackFileContainer))] + [TestFixture(typeof(PackFileContainer))] + internal class PackFileContainerTests_SystemFilePath : PackFileContainerTests_TestBase + { + public PackFileContainerTests_SystemFilePath(Type containerType) : base(containerType) { } + + [Test] + public void SystemFilePath_MatchesSeededPath() + { + Assert.That(_container.SystemFilePath, Is.EqualTo(@"c:\game\data")); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs index e442b1396..c32a12178 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs @@ -24,6 +24,7 @@ // When cleared, filter-expanded nodes are absorbed as user expansions and collapsed nodes are unloaded. using System.Windows.Input; +using System.IO; using AssetEditor.ViewModels; using Microsoft.Extensions.DependencyInjection; using Shared.Core.Events; @@ -341,7 +342,9 @@ private void CreatePackfiles(params (string Path, string FileName)[] files) var container = _packageFileService.CreateNewPackFileContainer("test.pack", PackFileVersion.PFH5, PackFileCAType.MOD); foreach (var (path, fileName) in files) { - container.AddOrUpdateFile(path.ToLowerInvariant(), PackFile.CreateFromASCII(fileName, fileName)); + var normalizedPath = path.ToLowerInvariant().Replace('/', '\\'); + var directory = Path.GetDirectoryName(normalizedPath)?.Replace('/', '\\') ?? string.Empty; + _packageFileService.AddFilesToPack(container, [new NewPackFileEntry(directory, PackFile.CreateFromASCII(fileName, fileName))]); } _packageFileService.AddContainer(container); From dffcbb04cd8f096c22f3e3490bf3af75c0465546 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 08:19:17 +0200 Subject: [PATCH 08/26] Code --- .../Containers/CachedPackFileContainer.cs | 25 ++++++++++++-- .../PackFileContainerTests_GetFullPath.cs | 33 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs index a4a3d87a0..d6f4a634b 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/CachedPackFileContainer.cs @@ -43,8 +43,29 @@ public bool ContainsFile(string path) public string? GetFullPath(PackFile file) { - var entry = _db.Files.FirstOrDefault(f => f.FileName == file.Name); - return entry?.RelativePath; + if (file.DataSource is PackedFileSource source) + { + var entry = _db.Files.FirstOrDefault(f => + f.SourcePackFilePath == source.Parent.FilePath && + f.Offset == source.Offset && + f.Size == source.Size && + f.IsEncrypted == source.IsEncrypted && + f.IsCompressed == source.IsCompressed && + f.CompressionFormat == (int)source.CompressionFormat && + f.UncompressedSize == source.UncompressedSize && + f.FileName.ToLower() == file.Name.ToLower()); + + if (entry != null) + return entry.RelativePath; + } + + var matchingPaths = _db.Files + .Where(f => f.FileName.ToLower() == file.Name.ToLower()) + .Select(f => f.RelativePath) + .Take(2) + .ToList(); + + return matchingPaths.Count == 1 ? matchingPaths[0] : null; } public List<(string FileName, PackFile Pack)> FindAllWithExtention(string extention) diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFullPath.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFullPath.cs index e7f17ab95..ba174af50 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFullPath.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_GetFullPath.cs @@ -1,5 +1,6 @@ using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.FileSources; namespace Shared.CoreTest.PackFiles.Models.Containers { @@ -23,5 +24,37 @@ public void GetFullPath_UnknownFile_ReturnsNull() var unknown = new PackFile("none.txt", null); Assert.That(_container.GetFullPath(unknown), Is.Null); } + + [Test] + public void GetFullPath_DuplicateFileName_ReturnsPathForExactSource() + { + var file = _container.FindFile("folder_b\\shared.txt")!; + + var path = _container.GetFullPath(file); + + Assert.That(path, Is.EqualTo("folder_b\\shared.txt")); + } + + [Test] + public void GetFullPath_DuplicateFileName_WithUnknownSource_ReturnsNullForCachedContainer() + { + IgnoreIfNotCached("Ambiguous filename fallback"); + + var unknownSourceFile = PackFile.CreateFromBytes("shared.txt", [1, 2, 3]); + + var path = _container.GetFullPath(unknownSourceFile); + + Assert.That(path, Is.Null); + } + + [Test] + public void GetFullPath_UniqueFileName_WithUnknownSource_UsesNameFallback() + { + var unknownSourceFile = PackFile.CreateFromBytes("file.txt", [1, 2, 3]); + + var path = _container.GetFullPath(unknownSourceFile); + + Assert.That(path, Is.EqualTo("folder\\file.txt")); + } } } From 786919a588bed49dc1fe85c0c7ba9151e3d91ad8 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 09:11:29 +0200 Subject: [PATCH 09/26] Code --- .../PackFileContainerCacheHelper.cs | 160 +++++++++++------- .../PackFileContainerTests_SearchFiles.cs | 63 +++++++ .../PackFileContainerTests_TestBase.cs | 27 +-- .../PackFileContainerCacheHelperTests.cs | 29 +++- 4 files changed, 202 insertions(+), 77 deletions(-) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/CacheDatabase/PackFileContainerCacheHelper.cs b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/CacheDatabase/PackFileContainerCacheHelper.cs index 950656807..53059ee8d 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/CacheDatabase/PackFileContainerCacheHelper.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/CacheDatabase/PackFileContainerCacheHelper.cs @@ -59,6 +59,23 @@ public static DbContextOptions CreateDbOptions(string dbFilePath .Options; } + public static DbContextOptions CreateDbOptionsFromConnectionString(string connectionString) + { + return new DbContextOptionsBuilder() + .UseSqlite(connectionString) + .Options; + } + + public static DbContextOptions CreateDbOptions(SqliteConnection connection) + { + if (connection.State != System.Data.ConnectionState.Open) + connection.Open(); + + return new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + } + private const int CurrentSchemaVersion = 3; public static void SaveCache(string fingerprint, PackFileContainer container, DbContextOptions dbOptions) @@ -72,84 +89,99 @@ public static void SaveCache(string fingerprint, PackFileContainer container, Db db.Database.EnsureCreated(); } - // Use raw SQLite for bulk insert - avoids EF change tracker memory and perf overhead - var connectionString = $"Data Source={GetDbFilePath(dbOptions)};Pooling=False"; - using var connection = new SqliteConnection(connectionString); - connection.Open(); - - using var transaction = connection.BeginTransaction(); + // Use raw SQLite for bulk insert - avoids EF change tracker memory and perf overhead. + // If dbOptions carries an explicit SqliteConnection (for tests/in-memory), reuse it. + var (connection, shouldDisposeConnection) = GetSqliteConnection(dbOptions); + if (connection.State != System.Data.ConnectionState.Open) + connection.Open(); - // Insert CacheInfo - using (var cmd = connection.CreateCommand()) + try { - cmd.Transaction = transaction; - cmd.CommandText = @"INSERT INTO CacheInfo (SchemaVersion, Fingerprint, ContainerName, SystemFilePath, SourcePackFilePaths) - VALUES ($schemaVersion, $fingerprint, $containerName, $systemFilePath, $sourcePackFilePaths)"; - cmd.Parameters.AddWithValue("$schemaVersion", CurrentSchemaVersion); - cmd.Parameters.AddWithValue("$fingerprint", fingerprint); - cmd.Parameters.AddWithValue("$containerName", container.Name); - cmd.Parameters.AddWithValue("$systemFilePath", container.SystemFilePath); - cmd.Parameters.AddWithValue("$sourcePackFilePaths", string.Join("|", container.SourcePackFilePaths)); - cmd.ExecuteNonQuery(); - } + using var transaction = connection.BeginTransaction(); - // Bulk insert files using a prepared statement - using (var cmd = connection.CreateCommand()) - { - cmd.Transaction = transaction; - cmd.CommandText = @"INSERT INTO FileList (RelativePath, FileName, Extension, FolderPath, SourcePackFilePath, Offset, Size, IsEncrypted, IsCompressed, CompressionFormat, UncompressedSize) - VALUES ($relativePath, $fileName, $extension, $folderPath, $sourcePackFilePath, $offset, $size, $isEncrypted, $isCompressed, $compressionFormat, $uncompressedSize)"; + // Insert CacheInfo + using (var cmd = connection.CreateCommand()) + { + cmd.Transaction = transaction; + cmd.CommandText = @"INSERT INTO CacheInfo (SchemaVersion, Fingerprint, ContainerName, SystemFilePath, SourcePackFilePaths) + VALUES ($schemaVersion, $fingerprint, $containerName, $systemFilePath, $sourcePackFilePaths)"; + cmd.Parameters.AddWithValue("$schemaVersion", CurrentSchemaVersion); + cmd.Parameters.AddWithValue("$fingerprint", fingerprint); + cmd.Parameters.AddWithValue("$containerName", container.Name); + cmd.Parameters.AddWithValue("$systemFilePath", container.SystemFilePath); + cmd.Parameters.AddWithValue("$sourcePackFilePaths", string.Join("|", container.SourcePackFilePaths)); + cmd.ExecuteNonQuery(); + } - var pRelativePath = cmd.Parameters.Add("$relativePath", SqliteType.Text); - var pFileName = cmd.Parameters.Add("$fileName", SqliteType.Text); - var pExtension = cmd.Parameters.Add("$extension", SqliteType.Text); - var pFolderPath = cmd.Parameters.Add("$folderPath", SqliteType.Text); - var pSourcePackFilePath = cmd.Parameters.Add("$sourcePackFilePath", SqliteType.Text); - var pOffset = cmd.Parameters.Add("$offset", SqliteType.Integer); - var pSize = cmd.Parameters.Add("$size", SqliteType.Integer); - var pIsEncrypted = cmd.Parameters.Add("$isEncrypted", SqliteType.Integer); - var pIsCompressed = cmd.Parameters.Add("$isCompressed", SqliteType.Integer); - var pCompressionFormat = cmd.Parameters.Add("$compressionFormat", SqliteType.Integer); - var pUncompressedSize = cmd.Parameters.Add("$uncompressedSize", SqliteType.Integer); - - cmd.Prepare(); - - foreach (var (relativePath, packFile) in container.GetAllFiles()) + // Bulk insert files using a prepared statement + using (var cmd = connection.CreateCommand()) { - if (packFile.DataSource is not PackedFileSource source) - continue; - - var lastSep = relativePath.LastIndexOf(Path.DirectorySeparatorChar); - var folderPath = lastSep == -1 ? "" : relativePath.Substring(0, lastSep); - - pRelativePath.Value = relativePath; - pFileName.Value = packFile.Name; - pExtension.Value = Path.GetExtension(relativePath).ToLower(); - pFolderPath.Value = folderPath; - pSourcePackFilePath.Value = source.Parent.FilePath; - pOffset.Value = source.Offset; - pSize.Value = source.Size; - pIsEncrypted.Value = source.IsEncrypted ? 1 : 0; - pIsCompressed.Value = source.IsCompressed ? 1 : 0; - pCompressionFormat.Value = (int)source.CompressionFormat; - pUncompressedSize.Value = (long)source.UncompressedSize; + cmd.Transaction = transaction; + cmd.CommandText = @"INSERT INTO FileList (RelativePath, FileName, Extension, FolderPath, SourcePackFilePath, Offset, Size, IsEncrypted, IsCompressed, CompressionFormat, UncompressedSize) + VALUES ($relativePath, $fileName, $extension, $folderPath, $sourcePackFilePath, $offset, $size, $isEncrypted, $isCompressed, $compressionFormat, $uncompressedSize)"; - cmd.ExecuteNonQuery(); + var pRelativePath = cmd.Parameters.Add("$relativePath", SqliteType.Text); + var pFileName = cmd.Parameters.Add("$fileName", SqliteType.Text); + var pExtension = cmd.Parameters.Add("$extension", SqliteType.Text); + var pFolderPath = cmd.Parameters.Add("$folderPath", SqliteType.Text); + var pSourcePackFilePath = cmd.Parameters.Add("$sourcePackFilePath", SqliteType.Text); + var pOffset = cmd.Parameters.Add("$offset", SqliteType.Integer); + var pSize = cmd.Parameters.Add("$size", SqliteType.Integer); + var pIsEncrypted = cmd.Parameters.Add("$isEncrypted", SqliteType.Integer); + var pIsCompressed = cmd.Parameters.Add("$isCompressed", SqliteType.Integer); + var pCompressionFormat = cmd.Parameters.Add("$compressionFormat", SqliteType.Integer); + var pUncompressedSize = cmd.Parameters.Add("$uncompressedSize", SqliteType.Integer); + + cmd.Prepare(); + + foreach (var (relativePath, packFile) in container.GetAllFiles()) + { + if (packFile.DataSource is not PackedFileSource source) + continue; + + var lastSep = relativePath.LastIndexOf(Path.DirectorySeparatorChar); + var folderPath = lastSep == -1 ? "" : relativePath.Substring(0, lastSep); + + pRelativePath.Value = relativePath; + pFileName.Value = packFile.Name; + pExtension.Value = Path.GetExtension(relativePath).ToLower(); + pFolderPath.Value = folderPath; + pSourcePackFilePath.Value = source.Parent.FilePath; + pOffset.Value = source.Offset; + pSize.Value = source.Size; + pIsEncrypted.Value = source.IsEncrypted ? 1 : 0; + pIsCompressed.Value = source.IsCompressed ? 1 : 0; + pCompressionFormat.Value = (int)source.CompressionFormat; + pUncompressedSize.Value = (long)source.UncompressedSize; + + cmd.ExecuteNonQuery(); + } } - } - transaction.Commit(); - _logger.Here().Information($"Cache saved successfully for '{container.Name}'"); + transaction.Commit(); + _logger.Here().Information($"Cache saved successfully for '{container.Name}'"); + } + finally + { + if (shouldDisposeConnection) + connection.Dispose(); + } } - private static string GetDbFilePath(DbContextOptions dbOptions) + private static (SqliteConnection Connection, bool ShouldDisposeConnection) GetSqliteConnection(DbContextOptions dbOptions) { var relationalOptions = dbOptions.Extensions .OfType() .FirstOrDefault(); - var connStr = relationalOptions?.ConnectionString ?? ""; - var builder = new SqliteConnectionStringBuilder(connStr); - return builder.DataSource; + + if (relationalOptions?.Connection is SqliteConnection sqliteConnection) + return (sqliteConnection, false); + + var connectionString = relationalOptions?.ConnectionString; + if (string.IsNullOrWhiteSpace(connectionString)) + throw new InvalidOperationException("Unable to resolve SQLite connection from DbContextOptions."); + + return (new SqliteConnection(connectionString), true); } public static CachedPackFileContainer? LoadContainerFromCache(string dbFilePath, string expectedFingerprint) diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs index 8e67460a8..b8278c865 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_SearchFiles.cs @@ -1,4 +1,9 @@ +using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.PackFiles.Serialization.CacheDatabase; +using Shared.Core.PackFiles.Utility; +using Microsoft.Data.Sqlite; namespace Shared.CoreTest.PackFiles.Models.Containers { @@ -37,5 +42,63 @@ public void SearchFiles_EmptyExtensionList_TreatedAsNoFilter() var results = _container.SearchFiles(null, []); Assert.That(results.Count, Is.EqualTo(15)); } + + [Test] + public void SearchFiles_ExtensionFilter_MatchesFilenameSubstrings_ForLegacyWemVariants() + { + var sourceContainer = new PackFileContainer("SearchFilesVariants") + { + IsCaPackFile = true, + SystemFilePath = @"c:\game\data" + }; + sourceContainer.SourcePackFilePaths.Add(@"c:\game\data\pack1.pack"); + + var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\pack1.pack" }; + sourceContainer.AddOrUpdateFile("audio\\voice.wem", new PackFile("voice.wem", new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("audio\\voice.wem_temp", new PackFile("voice.wem_temp", new PackedFileSource(parent, 10, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("audio\\voice.wem.{sdf}", new PackFile("voice.wem.{sdf}", new PackedFileSource(parent, 20, 10, false, false, CompressionFormat.None, 0))); + sourceContainer.AddOrUpdateFile("audio\\voice.txt", new PackFile("voice.txt", new PackedFileSource(parent, 30, 10, false, false, CompressionFormat.None, 0))); + + SqliteConnection? keepAliveConnection = null; + IPackFileContainer container; + try + { + if (IsCachedContainer) + { + var dbName = "SearchFilesVariants_" + Guid.NewGuid().ToString("N"); + var connectionString = new SqliteConnectionStringBuilder + { + DataSource = dbName, + Mode = SqliteOpenMode.Memory, + Cache = SqliteCacheMode.Shared + }.ToString(); + + // Keep one connection alive so the shared in-memory DB remains available + // while SaveCache and LoadContainerFromCache use separate EF/raw connections. + keepAliveConnection = new SqliteConnection(connectionString); + keepAliveConnection.Open(); + + var dbOptions = PackFileContainerCacheHelper.CreateDbOptionsFromConnectionString(connectionString); + PackFileContainerCacheHelper.SaveCache("variants_fp", sourceContainer, dbOptions); + container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "variants_fp")!; + } + else + { + container = sourceContainer; + } + + var results = container.SearchFiles(null, [".wem"]); + var paths = results.Select(x => x.Path).ToList(); + + Assert.That(paths, Does.Contain(@"audio\voice.wem")); + Assert.That(paths, Does.Contain(@"audio\voice.wem_temp")); + Assert.That(paths, Does.Contain(@"audio\voice.wem.{sdf}")); + Assert.That(paths, Does.Not.Contain(@"audio\voice.txt")); + } + finally + { + keepAliveConnection?.Dispose(); + } + } } } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs index 9348c39f0..c85d3769d 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/Containers/PackFileContainerTests_TestBase.cs @@ -1,4 +1,5 @@ -using Shared.Core.PackFiles.Models; +using Microsoft.Data.Sqlite; +using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.Containers; using Shared.Core.PackFiles.Models.FileSources; using Shared.Core.PackFiles.Serialization.CacheDatabase; @@ -10,8 +11,7 @@ internal abstract class PackFileContainerTests_TestBase { private readonly bool _useCachedContainer; - protected string _tempDir = null!; - protected string _dbFilePath = null!; + private SqliteConnection? _cacheKeepAliveConnection; protected IPackFileContainerInternal _container = null!; protected bool IsCachedContainer => _useCachedContainer; @@ -23,10 +23,6 @@ protected PackFileContainerTests_TestBase(Type containerType) [SetUp] public void Setup() { - _tempDir = Path.Combine(Path.GetTempPath(), "CachedContainerTests_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempDir); - _dbFilePath = Path.Combine(_tempDir, "test.db"); - var sourceContainer = new PackFileContainer("TestCache") { IsCaPackFile = true, @@ -66,9 +62,20 @@ public void Setup() sourceContainer.AddOrUpdateFile("compressed\\data.bin", new PackFile("data.bin", new PackedFileSource(parent, 1000, 500, true, true, CompressionFormat.Lz4, 2000))); - var dbOptions = PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); if (_useCachedContainer) { + var dbName = "CachedContainerTests_" + Guid.NewGuid().ToString("N"); + var connectionString = new SqliteConnectionStringBuilder + { + DataSource = dbName, + Mode = SqliteOpenMode.Memory, + Cache = SqliteCacheMode.Shared + }.ToString(); + + _cacheKeepAliveConnection = new SqliteConnection(connectionString); + _cacheKeepAliveConnection.Open(); + + var dbOptions = PackFileContainerCacheHelper.CreateDbOptionsFromConnectionString(connectionString); PackFileContainerCacheHelper.SaveCache("test_fp", sourceContainer, dbOptions); _container = PackFileContainerCacheHelper.LoadContainerFromCache(dbOptions, "test_fp")!; } @@ -81,8 +88,8 @@ public void Setup() [TearDown] public void TearDown() { - if (Directory.Exists(_tempDir)) - Directory.Delete(_tempDir, true); + _cacheKeepAliveConnection?.Dispose(); + _cacheKeepAliveConnection = null; } protected void IgnoreIfNotCached(string scenario) diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs index 83c0a9948..a8503b95c 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.Containers; using Shared.Core.PackFiles.Models.FileSources; @@ -11,6 +12,7 @@ internal class PackFileContainerCacheHelperTests { private string _tempDir; private string _dbFilePath; + private List _inMemoryKeepAliveConnections; [SetUp] public void Setup() @@ -18,16 +20,37 @@ public void Setup() _tempDir = Path.Combine(Path.GetTempPath(), "AssetEditorCacheTests_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); _dbFilePath = Path.Combine(_tempDir, "test_cache.db"); + _inMemoryKeepAliveConnections = []; } [TearDown] public void TearDown() { + foreach (var connection in _inMemoryKeepAliveConnections) + connection.Dispose(); + if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } private DbContextOptions CreateTestDbOptions() + { + var dbName = "CacheHelperTests_" + Guid.NewGuid().ToString("N"); + var connectionString = new SqliteConnectionStringBuilder + { + DataSource = dbName, + Mode = SqliteOpenMode.Memory, + Cache = SqliteCacheMode.Shared + }.ToString(); + + var keepAliveConnection = new SqliteConnection(connectionString); + keepAliveConnection.Open(); + _inMemoryKeepAliveConnections.Add(keepAliveConnection); + + return PackFileContainerCacheHelper.CreateDbOptionsFromConnectionString(connectionString); + } + + private DbContextOptions CreateFileDbOptions() { return PackFileContainerCacheHelper.CreateDbOptions(_dbFilePath); } @@ -247,7 +270,7 @@ public void TryLoadFromCache_ReturnsContainerWhenValid() container.AddOrUpdateFile("test\\file.txt", new PackFile("file.txt", new PackedFileSource(parent, 0, 100, false, false, CompressionFormat.None, 0))); - var dbOptions = CreateTestDbOptions(); + var dbOptions = CreateFileDbOptions(); PackFileContainerCacheHelper.SaveCache("fp_try", container, dbOptions); var result = PackFileContainerCacheHelper.TryLoadFromCache(_dbFilePath, "fp_try"); @@ -312,7 +335,7 @@ public void SaveCache_EmptyContainer_RoundTrips() public void SaveCache_OverwritesExistingCache() { var parent = new PackedFileSourceParent { FilePath = @"c:\game\pack.pack" }; - var dbOptions = CreateTestDbOptions(); + var dbOptions = CreateFileDbOptions(); // Save first version var container1 = new PackFileContainer("Version1") { SystemFilePath = @"c:\game" }; From 2bc97b3b6c64babd29585e5943e0f1faebf99f4a Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 10:12:55 +0200 Subject: [PATCH 10/26] Update to context menu and bugfix --- .../SceneExplorerContextMenuHandler.cs | 16 +++--- .../ContextMenu/ContextMenuItem.cs | 20 ------- .../PackFileTree/PackFileBrowserViewModel.cs | 26 ++++----- .../PackFileBrowserViewModelTests.cs | 56 +++++++++++++++++++ 4 files changed, 74 insertions(+), 44 deletions(-) diff --git a/Editors/Kitbashing/KitbasherEditor/Core/SceneExplorer/SceneExplorerContextMenuHandler.cs b/Editors/Kitbashing/KitbasherEditor/Core/SceneExplorer/SceneExplorerContextMenuHandler.cs index 5b4753fa1..7bfea829e 100644 --- a/Editors/Kitbashing/KitbasherEditor/Core/SceneExplorer/SceneExplorerContextMenuHandler.cs +++ b/Editors/Kitbashing/KitbasherEditor/Core/SceneExplorer/SceneExplorerContextMenuHandler.cs @@ -22,7 +22,7 @@ public partial class SceneExplorerContextMenuHandler : ObservableObject, IDispos ISceneNode _activeNode; IEnumerable _activeNodes; - [ObservableProperty] ObservableCollection _items = []; + [ObservableProperty] ObservableCollection _items = []; public SceneExplorerContextMenuHandler(IEventHub eventHub, SceneManager sceneManager, CommandFactory commandFactory, SelectionManager selectionManager) { @@ -49,26 +49,26 @@ void OnSelectionChanged(SceneNodeSelectedEvent selectionChangedEvent) return; if (CanMakeEditable(_activeNode)) - Items.Add(new ContextMenuItem("Make Editable", new RelayCommand(MakeEditable))); + Items.Add(new ContextMenuItem2("Make Editable", MakeEditable)); if (IsUngroupable(_activeNode)) - Items.Add(new ContextMenuItem("Ungroup", new RelayCommand(Ungroup))); + Items.Add(new ContextMenuItem2("Ungroup", Ungroup)); if (IsLockable(_activeNode)) - Items.Add(new ContextMenuItem("Lock", new RelayCommand(ToggleLock))); + Items.Add(new ContextMenuItem2("Lock", ToggleLock)); else if (IsUnlockable(_activeNode)) - Items.Add(new ContextMenuItem("Unlock", new RelayCommand(ToggleLock))); + Items.Add(new ContextMenuItem2("Unlock", ToggleLock)); if (Items.Count != 0) Items.Add(null); - Items.Add(new ContextMenuItem("Invert Selection", new RelayCommand(InvertSelection))); - Items.Add(new ContextMenuItem("Select Similarly Named", new RelayCommand(SelectSimilar))); + Items.Add(new ContextMenuItem2("Invert Selection", InvertSelection)); + Items.Add(new ContextMenuItem2("Select Similarly Named", SelectSimilar)); if (IsRemovable(_activeNode)) { if (Items.Count != 0) Items.Add(null); - Items.Add(new ContextMenuItem("Remove", new RelayCommand(RemoveNode))); + Items.Add(new ContextMenuItem2("Remove", RemoveNode)); } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs index 01bc1e241..6499353f9 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs @@ -5,26 +5,6 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu { - public class ContextMenuItem - { - public string Name { get; set; } - public ICommand? Command { get; set; } - public ObservableCollection ContextMenu { get; set; } = []; - - public ContextMenuItem(string name = "", ICommand? command = null) - { - Name = name; - Command = command; - } - - public override string ToString() - { - return Name; - } - } - - - public class ContextMenuItem2 { public string Name { get; set; } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs index 3e77d95ad..a37e21d86 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs @@ -247,6 +247,9 @@ private void AddFiles(IPackFileContainer container, List files) TreeNode newNode; if (numSeperators == 0) { + // EnsureChildrenPopulated runs inside InsertChildSorted and may load the + // just-added file from the container, so remove any duplicate after population. + root.EnsureChildrenPopulated(); RemoveExistingFileNode(root, item.Name, item); newNode = new TreeNode(item.Name, NodeType.File, container, root, item); InsertChildSorted(root, newNode); @@ -255,19 +258,13 @@ private void AddFiles(IPackFileContainer container, List files) { var directory = fullPath.Substring(0, directoryEnd); var folder = GetNodeFromPath(root, directory)!; - newNode = new TreeNode(item.Name, NodeType.File, container, folder, item); - var existingFile = folder.BackingChildren.FirstOrDefault(node => node.Name == item.Name && node.NodeType == NodeType.File); - if (existingFile != null) - { - folder.RemoveChild(existingFile); - if (folder.Children.Contains(existingFile)) - { - folder.Children.Remove(existingFile); - existingFile.RemoveSelf(); - } - } + // Populate the folder first so that any duplicate loaded from the container + // by EnsureChildrenPopulated (inside InsertChildSorted) is visible here. + folder.EnsureChildrenPopulated(); + RemoveExistingFileNode(folder, item.Name, item); + newNode = new TreeNode(item.Name, NodeType.File, container, folder, item); InsertChildSorted(folder, newNode); } @@ -514,11 +511,8 @@ private static void RemoveExistingFileNode(TreeNode parent, string fileName, Pac return; parent.RemoveChild(existingFile); - if (parent.Children.Contains(existingFile)) - { - parent.Children.Remove(existingFile); - existingFile.RemoveSelf(); - } + parent.Children.Remove(existingFile); + existingFile.RemoveSelf(); } } } diff --git a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs index c32a12178..d99ccad09 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs @@ -738,6 +738,62 @@ public void DoubleClickCollapseAndReexpand_ChildrenRemainVisible() file1 = _viewModel.GetFromPath(root, "foldera\\sub\\file1.txt"); Assert.That(file1, Is.Not.Null, "file1 should be visible inside expanded sub folder"); } + + [Test] + public void OverwriteFileInCollapsedFolder_NoDuplicateNodeInTree() + { + // Arrange: file exists in a folder that is NOT expanded (lazy state). + CreatePackfiles(("foldera\\file.txt", "file.txt")); + var root = _viewModel.Files[0]; + var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); + + // Folder is collapsed — BackingChildren are NOT yet loaded. + Assert.That(root.IsNodeExpanded, Is.False); + + // Act: add a second file with the same name (simulates Save As / overwrite import). + var replacement = PackFile.CreateFromASCII("file.txt", "updated content"); + _packageFileService.AddFilesToPack(container, [new NewPackFileEntry("foldera", replacement)]); + + // Expand to materialise children and assert only ONE node with that name exists. + root.IsNodeExpanded = true; + var folderA = _viewModel.GetFromPath(root, "foldera"); + Assert.That(folderA, Is.Not.Null); + folderA.IsNodeExpanded = true; + + var duplicates = folderA.Children + .Where(n => n.NodeType == NodeType.File && n.Name.Equals("file.txt", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + Assert.That(duplicates.Count, Is.EqualTo(1), "Overwriting a file in a collapsed folder must not create a duplicate node"); + Assert.That(duplicates[0].Item, Is.SameAs(replacement), "The surviving node should reference the replacement PackFile"); + } + + [Test] + public void OverwriteFileInExpandedFolder_NoDuplicateNodeInTree() + { + // Arrange: file exists in a folder that IS already expanded (children materialised). + CreatePackfiles(("foldera\\file.txt", "file.txt")); + var root = _viewModel.Files[0]; + var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); + + root.IsNodeExpanded = true; + var folderA = _viewModel.GetFromPath(root, "foldera"); + Assert.That(folderA, Is.Not.Null); + folderA.IsNodeExpanded = true; + + Assert.That(folderA.Children.Count(n => n.NodeType == NodeType.File), Is.EqualTo(1)); + + // Act: add a second file with the same name (simulates overwrite while folder is visible). + var replacement = PackFile.CreateFromASCII("file.txt", "updated content"); + _packageFileService.AddFilesToPack(container, [new NewPackFileEntry("foldera", replacement)]); + + var duplicates = folderA.Children + .Where(n => n.NodeType == NodeType.File && n.Name.Equals("file.txt", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + Assert.That(duplicates.Count, Is.EqualTo(1), "Overwriting a file in an expanded folder must not create a duplicate node"); + Assert.That(duplicates[0].Item, Is.SameAs(replacement), "The surviving node should reference the replacement PackFile"); + } } } From 11fafd6cc852d6691d09800da3dc8c2de5b4e581 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 10:18:29 +0200 Subject: [PATCH 11/26] Renamed class --- .../SceneExplorerContextMenuHandler.cs | 16 +++++++-------- .../ContextMenu/ContextMenuBuilder.cs | 20 +++++++++---------- .../ContextMenu/ContextMenuItem.cs | 6 +++--- .../MainApplicationContextMenuBuilder.cs | 6 +++--- .../ContextMenu/NoContextMenuBuilder.cs | 2 +- .../ContextMenu/SimpleContextMenuBuilder.cs | 2 +- .../PackFileTree/PackFileBrowserViewModel.cs | 4 ++-- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Editors/Kitbashing/KitbasherEditor/Core/SceneExplorer/SceneExplorerContextMenuHandler.cs b/Editors/Kitbashing/KitbasherEditor/Core/SceneExplorer/SceneExplorerContextMenuHandler.cs index 7bfea829e..b6ec35d9c 100644 --- a/Editors/Kitbashing/KitbasherEditor/Core/SceneExplorer/SceneExplorerContextMenuHandler.cs +++ b/Editors/Kitbashing/KitbasherEditor/Core/SceneExplorer/SceneExplorerContextMenuHandler.cs @@ -22,7 +22,7 @@ public partial class SceneExplorerContextMenuHandler : ObservableObject, IDispos ISceneNode _activeNode; IEnumerable _activeNodes; - [ObservableProperty] ObservableCollection _items = []; + [ObservableProperty] ObservableCollection _items = []; public SceneExplorerContextMenuHandler(IEventHub eventHub, SceneManager sceneManager, CommandFactory commandFactory, SelectionManager selectionManager) { @@ -49,26 +49,26 @@ void OnSelectionChanged(SceneNodeSelectedEvent selectionChangedEvent) return; if (CanMakeEditable(_activeNode)) - Items.Add(new ContextMenuItem2("Make Editable", MakeEditable)); + Items.Add(new ContextMenuItem("Make Editable", MakeEditable)); if (IsUngroupable(_activeNode)) - Items.Add(new ContextMenuItem2("Ungroup", Ungroup)); + Items.Add(new ContextMenuItem("Ungroup", Ungroup)); if (IsLockable(_activeNode)) - Items.Add(new ContextMenuItem2("Lock", ToggleLock)); + Items.Add(new ContextMenuItem("Lock", ToggleLock)); else if (IsUnlockable(_activeNode)) - Items.Add(new ContextMenuItem2("Unlock", ToggleLock)); + Items.Add(new ContextMenuItem("Unlock", ToggleLock)); if (Items.Count != 0) Items.Add(null); - Items.Add(new ContextMenuItem2("Invert Selection", InvertSelection)); - Items.Add(new ContextMenuItem2("Select Similarly Named", SelectSimilar)); + Items.Add(new ContextMenuItem("Invert Selection", InvertSelection)); + Items.Add(new ContextMenuItem("Select Similarly Named", SelectSimilar)); if (IsRemovable(_activeNode)) { if (Items.Count != 0) Items.Add(null); - Items.Add(new ContextMenuItem2("Remove", RemoveNode)); + Items.Add(new ContextMenuItem("Remove", RemoveNode)); } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuBuilder.cs index 3dfdb464a..21ffc89c9 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuBuilder.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuBuilder.cs @@ -29,7 +29,7 @@ public ContextMenuFactory(IEnumerable builders) public interface IContextMenuBuilder { ContextMenuType Type { get; } - public ObservableCollection Build(TreeNode? node); + public ObservableCollection Build(TreeNode? node); } public abstract class ContextMenuBuilder : IContextMenuBuilder @@ -44,15 +44,15 @@ public ContextMenuBuilder(ContextMenuType type, IUiCommandFactory commandFactory _commandFactory = commandFactory; } - protected abstract void Create(ContextMenuItem2 rootNode, TreeNode selectedNode); + protected abstract void Create(ContextMenuItem rootNode, TreeNode selectedNode); - public ObservableCollection Build(TreeNode? node) + public ObservableCollection Build(TreeNode? node) { - var output = new ObservableCollection(); + var output = new ObservableCollection(); if (node == null) return output; - var placeholderRoot = new ContextMenuItem2("Root", null); + var placeholderRoot = new ContextMenuItem("Root", null); Create(placeholderRoot, node); foreach (var item in placeholderRoot.ContextMenu) @@ -62,7 +62,7 @@ public ObservableCollection Build(TreeNode? node) } - protected void Add(TreeNode? node, ContextMenuItem2 parent) where T : IContextMenuCommand + protected void Add(TreeNode? node, ContextMenuItem parent) where T : IContextMenuCommand { var instance = _commandFactory.Create(); @@ -70,18 +70,18 @@ protected void Add(TreeNode? node, ContextMenuItem2 parent) where T : IContex return; var name = instance.GetDisplayName(node); - var item = new ContextMenuItem2(name, () => instance.Execute(node)); + var item = new ContextMenuItem(name, () => instance.Execute(node)); parent.ContextMenu.Add(item); } - protected void AddSeperator(ContextMenuItem2 parent) + protected void AddSeperator(ContextMenuItem parent) { parent.ContextMenu.Add(null); } - public ContextMenuItem2 AddChildMenu(string name, ContextMenuItem2 parent) + public ContextMenuItem AddChildMenu(string name, ContextMenuItem parent) { - var newItem = new ContextMenuItem2(name, null); + var newItem = new ContextMenuItem(name, null); parent.ContextMenu.Add(newItem); return newItem; diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs index 6499353f9..eb60e9d69 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs @@ -5,13 +5,13 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu { - public class ContextMenuItem2 + public class ContextMenuItem { public string Name { get; set; } public ICommand? Command { get; set; } - public ObservableCollection ContextMenu { get; set; } = []; + public ObservableCollection ContextMenu { get; set; } = []; - public ContextMenuItem2(string name, Action? action) + public ContextMenuItem(string name, Action? action) { Name = name; if (action != null) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs index cad77f5c8..87f64f38d 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs @@ -13,7 +13,7 @@ public MainApplicationContextMenuBuilder(IPackFileService packFileService, IUiCo _packFileService = packFileService; } - protected override void Create(ContextMenuItem2 rootNode, TreeNode selectedNode) + protected override void Create(ContextMenuItem rootNode, TreeNode selectedNode) { var nodeType = selectedNode.NodeType; switch (nodeType) @@ -30,7 +30,7 @@ protected override void Create(ContextMenuItem2 rootNode, TreeNode selectedNode) } } - void CreateForFile(ContextMenuItem2 rootNode, TreeNode selectedNode) + void CreateForFile(ContextMenuItem rootNode, TreeNode selectedNode) { if (_packFileService.GetEditablePack() != selectedNode.FileOwner) Add(selectedNode, rootNode); @@ -58,7 +58,7 @@ void CreateForFile(ContextMenuItem2 rootNode, TreeNode selectedNode) Add(selectedNode, reportsFolder); } - void CreateForDirectory(ContextMenuItem2 rootNode, TreeNode selectedNode) + void CreateForDirectory(ContextMenuItem rootNode, TreeNode selectedNode) { var isCaPack = selectedNode.FileOwner.IsCaPackFile; var isCurrentEditable = _packFileService.GetEditablePack() == selectedNode.FileOwner; diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/NoContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/NoContextMenuBuilder.cs index 1ed78e57e..ff09cc3fd 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/NoContextMenuBuilder.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/NoContextMenuBuilder.cs @@ -9,6 +9,6 @@ public NoContextMenuBuilder(IUiCommandFactory commandFactory) : base(ContextMenu } - protected override void Create(ContextMenuItem2 rootNode, TreeNode selectedNode) { } + protected override void Create(ContextMenuItem rootNode, TreeNode selectedNode) { } } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/SimpleContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/SimpleContextMenuBuilder.cs index 91943af1a..ce454a3b0 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/SimpleContextMenuBuilder.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/SimpleContextMenuBuilder.cs @@ -9,7 +9,7 @@ public SimpleContextMenuBuilder(IUiCommandFactory commandFactory) : base(Context { } - protected override void Create(ContextMenuItem2 rootNode, TreeNode selectedNode) + protected override void Create(ContextMenuItem rootNode, TreeNode selectedNode) { var nodeType = selectedNode.NodeType; if (nodeType == NodeType.File) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs index a37e21d86..ba404f1f0 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; @@ -37,7 +37,7 @@ public partial class PackFileBrowserViewModel : ObservableObject, IDisposable, I public SearchFilter Filter { get; private set; } [ObservableProperty] TreeNode _selectedItem; - [ObservableProperty] ObservableCollection _contextMenu = []; + [ObservableProperty] ObservableCollection _contextMenu = []; public bool ShowFoldersOnly { get; } From 553335804183bac17832fdb868a9b9ad8ee5e349 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 10:29:56 +0200 Subject: [PATCH 12/26] Moved interfaces --- Editors/Reports/DependencyInjectionContainer.cs | 2 +- Editors/Reports/Geometry/RmvToTextReport.cs | 2 +- .../ContextMenu/Commands/IRmvToTextCommand.cs | 6 ------ .../PackFileTree/ContextMenu/ContextMenuItem.cs | 2 -- .../ContextMenu/External/IRmvToTextCommand.cs | 8 ++++++++ .../ContextMenu/MainApplicationContextMenuBuilder.cs | 9 ++++++++- 6 files changed, 18 insertions(+), 11 deletions(-) delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/IRmvToTextCommand.cs create mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IRmvToTextCommand.cs diff --git a/Editors/Reports/DependencyInjectionContainer.cs b/Editors/Reports/DependencyInjectionContainer.cs index 43cce9f32..edd16d4d7 100644 --- a/Editors/Reports/DependencyInjectionContainer.cs +++ b/Editors/Reports/DependencyInjectionContainer.cs @@ -6,7 +6,7 @@ using Editors.Reports.Geometry; using Microsoft.Extensions.DependencyInjection; using Shared.Core.DependencyInjection; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; namespace Editors.Reports { diff --git a/Editors/Reports/Geometry/RmvToTextReport.cs b/Editors/Reports/Geometry/RmvToTextReport.cs index d238bdc4a..2bcae0249 100644 --- a/Editors/Reports/Geometry/RmvToTextReport.cs +++ b/Editors/Reports/Geometry/RmvToTextReport.cs @@ -7,7 +7,7 @@ using Shared.GameFormats.RigidModel.LodHeader; using Shared.GameFormats.RigidModel.MaterialHeaders; using Shared.Ui.BaseDialogs.PackFileTree; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; namespace Editors.Reports.Geometry { diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/IRmvToTextCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/IRmvToTextCommand.cs deleted file mode 100644 index bd23f3c6b..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/IRmvToTextCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands -{ - public interface IRmvToTextCommand : IContextMenuCommand - { - } -} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs index eb60e9d69..7e861652a 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuItem.cs @@ -20,6 +20,4 @@ public ContextMenuItem(string name, Action? action) public override string ToString() => Name; } - - } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IRmvToTextCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IRmvToTextCommand.cs new file mode 100644 index 000000000..4a0555025 --- /dev/null +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IRmvToTextCommand.cs @@ -0,0 +1,8 @@ +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External +{ + public interface IRmvToTextCommand : IContextMenuCommand + { + } +} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs index 87f64f38d..0fff2b7a1 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs @@ -1,9 +1,17 @@ using Shared.Core.Events; using Shared.Core.PackFiles; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu { + + + + + + + public class MainApplicationContextMenuBuilder : ContextMenuBuilder { private readonly IPackFileService _packFileService; @@ -69,7 +77,6 @@ void CreateForDirectory(ContextMenuItem rootNode, TreeNode selectedNode) Add(selectedNode, rootNode); AddSeperator(rootNode); - if (!isCaPack && isCurrentEditable == false) Add(selectedNode, rootNode); From 39b72206e04eaefade96879230cd9a7632a6a91c Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 10:50:52 +0200 Subject: [PATCH 13/26] Movign files --- .../PackFileTree/PackFileBrowserViewModel.cs | 148 +++------------- .../TreeNodeManipulationHelper.cs | 57 ++++++ .../PackFileTree/TreeNodePathHelper.cs | 74 ++++++++ .../PackFileTree/TreeNodeStateHelper.cs | 23 +++ .../PackFileBrowserViewModelTestHelper.cs | 34 ++++ .../PackFileBrowserViewModelTests.cs | 163 +++++++++--------- 6 files changed, 296 insertions(+), 203 deletions(-) create mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeManipulationHelper.cs create mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodePathHelper.cs create mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeStateHelper.cs create mode 100644 Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTestHelper.cs diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs index ba404f1f0..39b94158b 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs @@ -51,18 +51,18 @@ public PackFileBrowserViewModel(ApplicationSettingsService applicationSettingsSe ShowFoldersOnly = showFoldersOnly; - _eventHub?.Register(this, MainEditablePackChanged); - _eventHub?.Register(this, PackFileContainerRemoved); - _eventHub?.Register(this, x => ReloadTree(x.Container)); - _eventHub?.Register(this, Database_PackFilesUpdated); - _eventHub?.Register(this, x => AddFiles(x.Container, x.AddedFiles)); - _eventHub?.Register(this, x => Database_PackFilesRemoved(x.Container, x.RemovedFiles)); - _eventHub?.Register(this, x => Database_PackFileFolderRemoved(x.Container, x.Folder)); - _eventHub?.Register(this, x => Database_PackFileFolderRenamed(x.Container, x.OldNodePath, x.NewNodePath)); - _eventHub?.Register(this, ContainerSaved); + _eventHub?.Register(this, OnMainEditablePackChanged); + _eventHub?.Register(this, OnPackFileContainerRemoved); + _eventHub?.Register(this, OnPackFileContainerAddedEvent); + _eventHub?.Register(this, OnPackFileContainerFilesUpdatedEvent); + _eventHub?.Register(this, x => OnPackFileContainerFilesAddedEvent(x.Container, x.AddedFiles)); + _eventHub?.Register(this, x => OnPackFileContainerFilesRemovedEvent(x.Container, x.RemovedFiles)); + _eventHub?.Register(this, x => OnPackFileContainerFolderRemovedEvent(x.Container, x.Folder)); + _eventHub?.Register(this, x => OnPackFileContainerFolderRenamedEvent(x.Container, x.OldNodePath, x.NewNodePath)); + _eventHub?.Register(this, OnPackFileContainerSavedEvent); Filter = new SearchFilter(Files, () => _treeRoots.Values); - Filter.ShowFoldersOnly = ShowFoldersOnly; + Filter.ShowFoldersOnly = showFoldersOnly; foreach (var item in _packFileService.GetAllPackfileContainers()) { var loadFile = true; @@ -85,7 +85,9 @@ partial void OnSelectedItemChanged(TreeNode value) NodeSelected?.Invoke(_selectedItem); } - private void Database_PackFileFolderRemoved(IPackFileContainer container, string folder) + private void OnPackFileContainerAddedEvent(PackFileContainerAddedEvent e) => ReloadTree(e.Container); + + private void OnPackFileContainerFolderRemovedEvent(IPackFileContainer container, string folder) { var root = GetRootNode(container); var nodeToDelete = GetNodeFromPath(root, folder, false); @@ -101,7 +103,7 @@ private void Database_PackFileFolderRemoved(IPackFileContainer container, string Filter.Reapply(); } - private void Database_PackFileFolderRenamed(IPackFileContainer container, string oldPath, string newPath) + private void OnPackFileContainerFolderRenamedEvent(IPackFileContainer container, string oldPath, string newPath) { var root = GetRootNode(container); var node = GetNodeFromPath(root, oldPath, false); @@ -118,27 +120,17 @@ private void Database_PackFileFolderRenamed(IPackFileContainer container, string Filter.Reapply(); } - private void ContainerSaved(PackFileContainerSavedEvent e) + private void OnPackFileContainerSavedEvent(PackFileContainerSavedEvent e) { var root = GetRootNode(e.Container); - ClearUnsavedOnLoadedNodes(root); + TreeNodeStateHelper.ClearUnsavedOnLoadedNodes(root); root.ForeachNode((node) => node.UnsavedChanged = false); Filter.Reapply(); } - private static void ClearUnsavedOnLoadedNodes(TreeNode node) - { - node.UnsavedChanged = false; - if (!node.ChildrenLoaded) - return; - - foreach (var child in node.BackingChildren) - ClearUnsavedOnLoadedNodes(child); - } - - private void Database_PackFilesRemoved(IPackFileContainer container, List files) + private void OnPackFileContainerFilesRemovedEvent(IPackFileContainer container, List files) { var root = GetRootNode(container); root.UnsavedChanged = true; @@ -158,7 +150,7 @@ private void Database_PackFilesRemoved(IPackFileContainer container, List files) + private void OnPackFileContainerFilesAddedEvent(IPackFileContainer container, List files) { var root = GetRootNode(container); root.UnsavedChanged = true; @@ -250,9 +242,9 @@ private void AddFiles(IPackFileContainer container, List files) // EnsureChildrenPopulated runs inside InsertChildSorted and may load the // just-added file from the container, so remove any duplicate after population. root.EnsureChildrenPopulated(); - RemoveExistingFileNode(root, item.Name, item); + TreeNodeManipulationHelper.RemoveExistingFileNode(root, item.Name, item); newNode = new TreeNode(item.Name, NodeType.File, container, root, item); - InsertChildSorted(root, newNode); + TreeNodeManipulationHelper.InsertChildSorted(root, newNode); } else { @@ -262,10 +254,10 @@ private void AddFiles(IPackFileContainer container, List files) // Populate the folder first so that any duplicate loaded from the container // by EnsureChildrenPopulated (inside InsertChildSorted) is visible here. folder.EnsureChildrenPopulated(); - RemoveExistingFileNode(folder, item.Name, item); + TreeNodeManipulationHelper.RemoveExistingFileNode(folder, item.Name, item); newNode = new TreeNode(item.Name, NodeType.File, container, folder, item); - InsertChildSorted(folder, newNode); + TreeNodeManipulationHelper.InsertChildSorted(folder, newNode); } newNode.UnsavedChanged = true; @@ -280,66 +272,6 @@ private void AddFiles(IPackFileContainer container, List files) Filter.Reapply(); } - public TreeNode? GetFromPath(TreeNode parent, string path) - { - if (path.Length == 0) - return parent; - - // First determine what the target is by searching through backing children - var target = FindInBackingChildren(parent, path); - if (target == null) - return null; - - if (target.NodeType == NodeType.File) - return GetFromPathViaMaterialization(parent, path); - - // Directory: only return if already materialized (visible in the WPF tree) - return FindInMaterializedChildren(parent, path); - } - - private static TreeNode? FindInBackingChildren(TreeNode parent, string path) - { - if (path.Length == 0) - return parent; - - parent.EnsureChildrenPopulated(); - - var separatorIndex = path.IndexOf(Path.DirectorySeparatorChar); - var nodeName = separatorIndex == -1 ? path : path.Substring(0, separatorIndex); - var remainingPath = separatorIndex == -1 ? string.Empty : path.Substring(separatorIndex + 1); - - var child = parent.BackingChildren.FirstOrDefault(x => x.Name == nodeName); - return child == null ? null : FindInBackingChildren(child, remainingPath); - } - - private static TreeNode? GetFromPathViaMaterialization(TreeNode parent, string path) - { - if (path.Length == 0) - return parent; - - parent.MaterializeChildren(); - - var separatorIndex = path.IndexOf(Path.DirectorySeparatorChar); - var nodeName = separatorIndex == -1 ? path : path.Substring(0, separatorIndex); - var remainingPath = separatorIndex == -1 ? string.Empty : path.Substring(separatorIndex + 1); - - var child = parent.Children.FirstOrDefault(x => x.Name == nodeName); - return child == null ? null : GetFromPathViaMaterialization(child, remainingPath); - } - - private static TreeNode? FindInMaterializedChildren(TreeNode parent, string path) - { - if (path.Length == 0) - return parent; - - var separatorIndex = path.IndexOf(Path.DirectorySeparatorChar); - var nodeName = separatorIndex == -1 ? path : path.Substring(0, separatorIndex); - var remainingPath = separatorIndex == -1 ? string.Empty : path.Substring(separatorIndex + 1); - - var child = parent.Children.FirstOrDefault(x => x.Name == nodeName); - return child == null ? null : FindInMaterializedChildren(child, remainingPath); - } - private TreeNode? GetNodeFromPath(TreeNode parent, string path, bool createIfMissing = true) { var numSeperators = path.Count(x => x == Path.DirectorySeparatorChar); @@ -368,7 +300,7 @@ private void AddFiles(IPackFileContainer container, List files) { var newNode = new TreeNode(nodeName, NodeType.Directory, parent.FileOwner, parent, () => Filter.HasActiveFilter); newNode.MarkChildrenLoaded(); - InsertChildSorted(parent, newNode); + TreeNodeManipulationHelper.InsertChildSorted(parent, newNode); return GetNodeFromPath(newNode, remainingStr, createIfMissing); } return null; @@ -441,7 +373,7 @@ private bool LoadChildrenFromContainer(TreeNode node, IPackFileContainer contain return true; } - private void PackFileContainerRemoved(PackFileContainerRemovedEvent e) + private void OnPackFileContainerRemoved(PackFileContainerRemovedEvent e) { if (_treeRoots.TryGetValue(e.Container, out var root)) { @@ -486,33 +418,5 @@ private TreeNode GetRootNode(IPackFileContainer container) { return _treeRoots[container]; } - - private static readonly Comparison TreeNodeComparison = (left, right) => - { - var nodeTypeComparison = left.NodeType.CompareTo(right.NodeType); - if (nodeTypeComparison != 0) - return nodeTypeComparison; - - return StringComparer.CurrentCultureIgnoreCase.Compare(left.Name, right.Name); - }; - - private static void InsertChildSorted(TreeNode parent, TreeNode child) - { - parent.EnsureChildrenPopulated(); - parent.AddChild(child); - parent.BackingChildren.Sort(TreeNodeComparison); - } - - private static void RemoveExistingFileNode(TreeNode parent, string fileName, PackFile packFile) - { - parent.EnsureChildrenPopulated(); - var existingFile = parent.BackingChildren.FirstOrDefault(node => node.NodeType == NodeType.File && (node.Item == packFile || node.Name == fileName)); - if (existingFile == null) - return; - - parent.RemoveChild(existingFile); - parent.Children.Remove(existingFile); - existingFile.RemoveSelf(); - } } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeManipulationHelper.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeManipulationHelper.cs new file mode 100644 index 000000000..3b68680a8 --- /dev/null +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeManipulationHelper.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using Shared.Core.PackFiles.Models; + +namespace Shared.Ui.BaseDialogs.PackFileTree +{ + /// + /// Helper class for tree node manipulation operations including sorting and removal. + /// + public static class TreeNodeManipulationHelper + { + /// + /// Comparison function for sorting tree nodes by type and then by name. + /// + public static readonly Comparison TreeNodeComparison = (left, right) => + { + var nodeTypeComparison = left.NodeType.CompareTo(right.NodeType); + if (nodeTypeComparison != 0) + return nodeTypeComparison; + + return StringComparer.CurrentCultureIgnoreCase.Compare(left.Name, right.Name); + }; + + /// + /// Inserts a child node into the parent's children collection in sorted order. + /// + /// The parent node + /// The child node to insert + public static void InsertChildSorted(TreeNode parent, TreeNode child) + { + parent.EnsureChildrenPopulated(); + parent.AddChild(child); + parent.BackingChildren.Sort(TreeNodeComparison); + } + + /// + /// Removes an existing file node from the parent's children collection. + /// + /// The parent node + /// The name of the file to remove + /// The pack file object to match + public static void RemoveExistingFileNode(TreeNode parent, string fileName, PackFile packFile) + { + parent.EnsureChildrenPopulated(); + var existingFile = parent.BackingChildren.FirstOrDefault(node => + node.NodeType == NodeType.File && + (node.Item == packFile || node.Name == fileName)); + + if (existingFile == null) + return; + + parent.RemoveChild(existingFile); + parent.Children.Remove(existingFile); + existingFile.RemoveSelf(); + } + } +} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodePathHelper.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodePathHelper.cs new file mode 100644 index 000000000..9cefcd4fe --- /dev/null +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodePathHelper.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; + +namespace Shared.Ui.BaseDialogs.PackFileTree +{ + /// + /// Helper class for tree node path navigation and lookup operations. + /// + public static class TreeNodePathHelper + { + /// + /// Recursively searches for a node in the backing children collection by path. + /// + /// The parent node to search from + /// The path to search for (e.g., "folder1/folder2/file") + /// The found node or null if not found + public static TreeNode? FindInBackingChildren(TreeNode parent, string path) + { + if (path.Length == 0) + return parent; + + parent.EnsureChildrenPopulated(); + + var separatorIndex = path.IndexOf(System.IO.Path.DirectorySeparatorChar); + var nodeName = separatorIndex == -1 ? path : path.Substring(0, separatorIndex); + var remainingPath = separatorIndex == -1 ? string.Empty : path.Substring(separatorIndex + 1); + + var child = parent.BackingChildren.FirstOrDefault(x => x.Name == nodeName); + return child == null ? null : FindInBackingChildren(child, remainingPath); + } + + /// + /// Recursively searches for a node in the materialized children collection by path, + /// materializing children as needed. + /// + /// The parent node to search from + /// The path to search for + /// The found node or null if not found + public static TreeNode? GetFromPathViaMaterialization(TreeNode parent, string path) + { + if (path.Length == 0) + return parent; + + parent.MaterializeChildren(); + + var separatorIndex = path.IndexOf(System.IO.Path.DirectorySeparatorChar); + var nodeName = separatorIndex == -1 ? path : path.Substring(0, separatorIndex); + var remainingPath = separatorIndex == -1 ? string.Empty : path.Substring(separatorIndex + 1); + + var child = parent.Children.FirstOrDefault(x => x.Name == nodeName); + return child == null ? null : GetFromPathViaMaterialization(child, remainingPath); + } + + /// + /// Recursively searches for a node in the materialized children collection by path. + /// Only returns results that are already materialized (visible in the WPF tree). + /// + /// The parent node to search from + /// The path to search for + /// The found node or null if not found + public static TreeNode? FindInMaterializedChildren(TreeNode parent, string path) + { + if (path.Length == 0) + return parent; + + var separatorIndex = path.IndexOf(System.IO.Path.DirectorySeparatorChar); + var nodeName = separatorIndex == -1 ? path : path.Substring(0, separatorIndex); + var remainingPath = separatorIndex == -1 ? string.Empty : path.Substring(separatorIndex + 1); + + var child = parent.Children.FirstOrDefault(x => x.Name == nodeName); + return child == null ? null : FindInMaterializedChildren(child, remainingPath); + } + } +} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeStateHelper.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeStateHelper.cs new file mode 100644 index 000000000..cc1ce18d4 --- /dev/null +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeStateHelper.cs @@ -0,0 +1,23 @@ +namespace Shared.Ui.BaseDialogs.PackFileTree +{ + /// + /// Helper class for tree node state management operations. + /// + public static class TreeNodeStateHelper + { + /// + /// Recursively clears the UnsavedChanged flag on a node and all its loaded children. + /// Only processes children that have already been loaded (materialized). + /// + /// The node to start clearing from + public static void ClearUnsavedOnLoadedNodes(TreeNode node) + { + node.UnsavedChanged = false; + if (!node.ChildrenLoaded) + return; + + foreach (var child in node.BackingChildren) + ClearUnsavedOnLoadedNodes(child); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTestHelper.cs b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTestHelper.cs new file mode 100644 index 000000000..009572334 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTestHelper.cs @@ -0,0 +1,34 @@ +using Shared.Ui.BaseDialogs.PackFileTree; + +namespace Shared.UiTest +{ + /// + /// Test utility helper for PackFileBrowserViewModel testing. + /// Provides helper methods for navigating the tree structure created by the view model. + /// + public static class PackFileBrowserViewModelTestHelper + { + /// + /// Finds a node by its path within the tree hierarchy. + /// + /// The parent node to search from + /// The path to search for (e.g., "folder1/folder2/file") + /// The found node or null if not found. For files, requires materialization. For directories, only returns if materialized. + public static TreeNode? GetFromPath(TreeNode parent, string path) + { + if (path.Length == 0) + return parent; + + // First determine what the target is by searching through backing children + var target = TreeNodePathHelper.FindInBackingChildren(parent, path); + if (target == null) + return null; + + if (target.NodeType == NodeType.File) + return TreeNodePathHelper.GetFromPathViaMaterialization(parent, path); + + // Directory: only return if already materialized (visible in the WPF tree) + return TreeNodePathHelper.FindInMaterializedChildren(parent, path); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs index d99ccad09..3e2697a3b 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs @@ -1,4 +1,4 @@ -// PackFileBrowserViewModel Lazy Loading Architecture: +// PackFileBrowserViewModel Lazy Loading Architecture: // // The tree is built lazily. When a container is added (via PackFileContainerAddedEvent), only // the root TreeNode is created with a child-loader delegate. Children are NOT loaded from @@ -10,15 +10,15 @@ // shows the expand arrow for unloaded directories. // // Event-driven updates from PackFileService: -// - PackFileContainerAddedEvent → ReloadTree: creates root node + lazy child loader -// - PackFileContainerRemovedEvent → removes container's root and TreeNode from Files -// - PackFileContainerFilesAddedEvent → AddFiles: inserts nodes into loaded branches, creates dirs as needed -// - PackFileContainerFilesRemovedEvent → removes nodes for deleted files -// - PackFileContainerFilesUpdatedEvent → updates Name/UnsavedChanged on nodes (for rename/save) -// - PackFileContainerFolderRemovedEvent → finds and removes folder node and subtree -// - PackFileContainerFolderRenamedEvent → finds folder by OLD path, renames leaf, marks unsaved -// - PackFileContainerSetAsMainEditableEvent → toggles IsMainEditabelPack on root TreeNodes -// - PackFileContainerSavedEvent → clears UnsavedChanged on loaded nodes only (avoids forcing full population) +// - PackFileContainerAddedEvent ? ReloadTree: creates root node + lazy child loader +// - PackFileContainerRemovedEvent ? removes container's root and TreeNode from Files +// - PackFileContainerFilesAddedEvent ? AddFiles: inserts nodes into loaded branches, creates dirs as needed +// - PackFileContainerFilesRemovedEvent ? removes nodes for deleted files +// - PackFileContainerFilesUpdatedEvent ? updates Name/UnsavedChanged on nodes (for rename/save) +// - PackFileContainerFolderRemovedEvent ? finds and removes folder node and subtree +// - PackFileContainerFolderRenamedEvent ? finds folder by OLD path, renames leaf, marks unsaved +// - PackFileContainerSetAsMainEditableEvent ? toggles IsMainEditabelPack on root TreeNodes +// - PackFileContainerSavedEvent ? clears UnsavedChanged on loaded nodes only (avoids forcing full population) // // SearchFilter: when active, calls EnsureFullyPopulated() to load entire subtree for regex matching. // When cleared, filter-expanded nodes are absorbed as user expansions and collapsed nodes are unloaded. @@ -71,8 +71,8 @@ public void DragAndDrop_FileInFolder() var packRootNode = _viewModel.Files[1]; // Act - var fileToMove = _viewModel.GetFromPath(packRootNode, @"animations\battle\humanoid01\2handed_hammer\stand\hu1_2hh_stand_idle_01.anim"); - var destinationNode = _viewModel.GetFromPath(packRootNode, @"animations"); + var fileToMove = PackFileBrowserViewModelTestHelper.GetFromPath(packRootNode, @"animations\battle\humanoid01\2handed_hammer\stand\hu1_2hh_stand_idle_01.anim"); + var destinationNode = PackFileBrowserViewModelTestHelper.GetFromPath(packRootNode, @"animations"); _viewModel.Drop(fileToMove, destinationNode); // Assert @@ -81,7 +81,7 @@ public void DragAndDrop_FileInFolder() Assert.That(movedFile, Is.Not.Null); // Get file node from - var movedNode = _viewModel.GetFromPath(packRootNode, @"animations\hu1_2hh_stand_idle_01.anim"); + var movedNode = PackFileBrowserViewModelTestHelper.GetFromPath(packRootNode, @"animations\hu1_2hh_stand_idle_01.anim"); Assert.That(movedNode, Is.Not.Null); Assert.That(movedNode.UnsavedChanged, Is.True); Assert.That(movedNode.Parent.UnsavedChanged, Is.True); @@ -95,12 +95,12 @@ public void OnlyRootExpandedByDefault() Assert.That(_viewModel.Files.Count, Is.EqualTo(1)); var root = _viewModel.Files[0]; - Assert.That(_viewModel.GetFromPath(root, "foldera"), Is.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"), Is.Null); root.IsNodeExpanded = true; - Assert.That(_viewModel.GetFromPath(root, "foldera"), Is.Not.Null); - Assert.That(_viewModel.GetFromPath(root, "folderb"), Is.Not.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"), Is.Not.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "folderb"), Is.Not.Null); } [Test] @@ -111,7 +111,7 @@ public void GetFileFromCollapsedNode() Assert.That(root.IsNodeExpanded, Is.False); - var fileNode = _viewModel.GetFromPath(root, "animations\\battle\\humanoid01\\test.anim"); + var fileNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "animations\\battle\\humanoid01\\test.anim"); Assert.That(fileNode, Is.Not.Null); Assert.That(fileNode.NodeType, Is.EqualTo(NodeType.File)); @@ -126,15 +126,15 @@ public void CollapsingNodeUnloadsItsChildren() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); folderA.IsNodeExpanded = true; - Assert.That(_viewModel.GetFromPath(root, "foldera\\sub"), Is.Not.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub"), Is.Not.Null); folderA.IsNodeExpanded = false; - Assert.That(_viewModel.GetFromPath(root, "foldera\\sub"), Is.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub"), Is.Null); } [Test] @@ -150,16 +150,16 @@ public void SearchMaterializesOnlyMatchingBranchAndClearsBackToLazyState() _viewModel.Filter.FilterText = "match_file"; Assert.That(root.IsNodeExpanded, Is.True); - Assert.That(_viewModel.GetFromPath(root, "foldera\\sub\\match_file.txt"), Is.Not.Null); - Assert.That(_viewModel.GetFromPath(root, "folderb"), Is.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub\\match_file.txt"), Is.Not.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "folderb"), Is.Null); _viewModel.Filter.FilterText = string.Empty; // After clearing, filter-expanded folders are preserved (absorbed as user expansions) Assert.That(root.IsNodeExpanded, Is.True); - Assert.That(_viewModel.GetFromPath(root, "foldera"), Is.Not.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"), Is.Not.Null); // folderB is now also visible and materialized since filter is cleared - Assert.That(_viewModel.GetFromPath(root, "folderb"), Is.Not.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "folderb"), Is.Not.Null); } [Test] @@ -174,7 +174,7 @@ public void SearchDoesNotCollapseNodesExpandedByUserBeforeFiltering() _viewModel.Filter.FilterText = string.Empty; Assert.That(root.IsNodeExpanded, Is.True); - Assert.That(_viewModel.GetFromPath(root, "foldera"), Is.Not.Null); + Assert.That(PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"), Is.Not.Null); } [Test] @@ -189,14 +189,14 @@ public void ClearingFilterKeepsExpandedFoldersAndShowsAllFiles() // User manually expands root and folderA root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); folderA.IsNodeExpanded = true; - // Search for a specific file — folderA/sub gets expanded by filter, folderB hidden + // Search for a specific file — folderA/sub gets expanded by filter, folderB hidden _viewModel.Filter.FilterText = "match_file"; - var sub = _viewModel.GetFromPath(root, "foldera\\sub"); + var sub = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub"); Assert.That(sub, Is.Not.Null); Assert.That(sub.IsNodeExpanded, Is.True, "sub should be expanded by filter"); @@ -208,18 +208,18 @@ public void ClearingFilterKeepsExpandedFoldersAndShowsAllFiles() Assert.That(folderA.IsNodeExpanded, Is.True, "folderA was user-expanded, should stay open"); // Filter-expanded nodes are preserved (absorbed) - sub = _viewModel.GetFromPath(root, "foldera\\sub"); + sub = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub"); Assert.That(sub, Is.Not.Null, "sub should still be materialized"); Assert.That(sub.IsNodeExpanded, Is.True, "sub was filter-expanded, now absorbed as user expansion"); // Previously hidden folder is now visible and materialized - var folderB = _viewModel.GetFromPath(root, "folderb"); + var folderB = PackFileBrowserViewModelTestHelper.GetFromPath(root, "folderb"); Assert.That(folderB, Is.Not.Null, "folderB should be materialized after filter cleared"); Assert.That(folderB.IsVisible, Is.True, "folderB should be visible after filter cleared"); // All files in expanded folders are visible - var matchFile = _viewModel.GetFromPath(root, "foldera\\sub\\match_file.txt"); - var otherA = _viewModel.GetFromPath(root, "foldera\\sub\\other_a.txt"); + var matchFile = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub\\match_file.txt"); + var otherA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub\\other_a.txt"); Assert.That(matchFile, Is.Not.Null, "match_file should be visible"); Assert.That(otherA, Is.Not.Null, "other_a should be visible after filter cleared"); } @@ -234,7 +234,7 @@ public void CtrlDoubleClickDirectoryExpandsAllChildren() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); // Intentionally set a different selected item to verify command uses clicked node argument. @@ -242,8 +242,8 @@ public void CtrlDoubleClickDirectoryExpandsAllChildren() _viewModel.SelectedItem = root; _viewModel.DoubleClickCommand.Execute(folderA); - var subA = _viewModel.GetFromPath(root, "foldera\\suba"); - var subB = _viewModel.GetFromPath(root, "foldera\\subb"); + var subA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\suba"); + var subB = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\subb"); Assert.That(folderA.IsNodeExpanded, Is.True); Assert.That(subA, Is.Not.Null); @@ -261,7 +261,7 @@ public void DeleteFileUsingPfs_EnsureTreeViewUpdated() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); folderA.IsNodeExpanded = true; @@ -288,7 +288,7 @@ public void RenameFileUsingPfs_EnsureTreeViewUpdated() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); folderA.IsNodeExpanded = true; @@ -302,8 +302,8 @@ public void RenameFileUsingPfs_EnsureTreeViewUpdated() _packageFileService.RenameFile(container, filePackFile, "file_renamed.txt"); // Assert: old node/path should be gone and new node/path should exist in the tree - var oldNode = _viewModel.GetFromPath(root, "foldera\\file.txt"); - var renamedNode = _viewModel.GetFromPath(root, "foldera\\file_renamed.txt"); + var oldNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\file.txt"); + var renamedNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\file_renamed.txt"); Assert.That(oldNode, Is.Null, "Old file path should not exist after rename"); Assert.That(renamedNode, Is.Not.Null, "Renamed file should exist in the tree view"); @@ -327,7 +327,7 @@ public void AddDuplicateNamedFileToNewFolderUsingPfs_EnsureTreeViewUpdated() var addedFile = _packageFileService.FindFile(saveAsTargetPath, container); var resolvedPathForAddedFile = _packageFileService.GetFullPath(copiedRigidModel, container); - var addedNode = _viewModel.GetFromPath(root, saveAsTargetPath); + var addedNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, saveAsTargetPath); Assert.Multiple(() => { @@ -357,15 +357,15 @@ public void RenameFolderUsingPfs_EnsureTreeViewUpdated() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); _packageFileService.RenameDirectory(container, "foldera", "folderb"); // Old path gone, new path available - var oldNode = _viewModel.GetFromPath(root, "foldera"); - var newNode = _viewModel.GetFromPath(root, "folderb"); + var oldNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); + var newNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "folderb"); Assert.That(oldNode, Is.Null, "Old folder path should not exist after rename"); Assert.That(newNode, Is.Not.Null, "Renamed folder should be accessible via the new name"); @@ -379,14 +379,14 @@ public void RenameFolderUsingPfs_NestedFolder_EnsureTreeViewUpdated() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var parent = _viewModel.GetFromPath(root, "parent"); + var parent = PackFileBrowserViewModelTestHelper.GetFromPath(root, "parent"); Assert.That(parent, Is.Not.Null); parent.IsNodeExpanded = true; var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); _packageFileService.RenameDirectory(container, "parent\\child", "renamed"); - var newNode = _viewModel.GetFromPath(root, "parent\\renamed"); + var newNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "parent\\renamed"); Assert.That(newNode, Is.Not.Null, "Nested renamed folder should be accessible"); Assert.That(newNode.UnsavedChanged, Is.True); } @@ -398,16 +398,16 @@ public void DeleteFolderUsingPfs_EnsureTreeViewUpdated() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); _packageFileService.DeleteFolder(container, "foldera"); - var deletedNode = _viewModel.GetFromPath(root, "foldera"); + var deletedNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(deletedNode, Is.Null, "Deleted folder should not exist in tree"); - var survivingNode = _viewModel.GetFromPath(root, "folderb"); + var survivingNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "folderb"); Assert.That(survivingNode, Is.Not.Null, "Non-deleted folder should still exist"); } @@ -468,11 +468,11 @@ public void ContainerSaved_ClearsUnsavedFlags() _packageFileService.AddFilesToPack(container, [new NewPackFileEntry("foldera", newFile)]); root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); Assert.That(root.UnsavedChanged, Is.True, "Root should be unsaved after adding file"); - // Simulate save via PFS — we can't call SavePackContainer without disk I/O, + // Simulate save via PFS — we can't call SavePackContainer without disk I/O, // so trigger the event directly through the event hub var eventHub = _runner.ServiceProvider.GetRequiredService(); eventHub.PublishGlobalEvent(new Shared.Core.Events.Global.PackFileContainerSavedEvent(container)); @@ -487,7 +487,7 @@ public void ContainerSaved_DoesNotForcePopulationOfUnloadedBranches() var root = _viewModel.Files[0]; var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); - // Root is collapsed — only root exists as materialized node + // Root is collapsed — only root exists as materialized node Assert.That(root.IsNodeExpanded, Is.False); var eventHub = _runner.ServiceProvider.GetRequiredService(); @@ -510,7 +510,7 @@ public void AddFileToRootLevel_EnsureTreeViewUpdated() var root = _viewModel.Files.First(x => x.Name == "test.pack"); root.IsNodeExpanded = true; - var fileNode = _viewModel.GetFromPath(root, "rootfile.txt"); + var fileNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "rootfile.txt"); Assert.That(fileNode, Is.Not.Null, "File added at root level should appear in tree"); Assert.That(fileNode.NodeType, Is.EqualTo(NodeType.File)); } @@ -522,10 +522,10 @@ public void DoubleClick_File_InvokesFileOpenEvent() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); folderA.IsNodeExpanded = true; - var fileNode = _viewModel.GetFromPath(root, "foldera\\file.txt"); + var fileNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\file.txt"); Assert.That(fileNode, Is.Not.Null); PackFile? openedFile = null; @@ -544,7 +544,7 @@ public void DoubleClick_Directory_TogglesExpansion() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); Assert.That(folderA.IsNodeExpanded, Is.False); @@ -563,24 +563,24 @@ public void MoveFileUsingPfs_EnsureTreeViewUpdated() root.IsNodeExpanded = true; var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); - var sourceFolder = _viewModel.GetFromPath(root, "source"); + var sourceFolder = PackFileBrowserViewModelTestHelper.GetFromPath(root, "source"); Assert.That(sourceFolder, Is.Not.Null); sourceFolder.IsNodeExpanded = true; - var fileNode = _viewModel.GetFromPath(root, "source\\file.txt"); + var fileNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "source\\file.txt"); Assert.That(fileNode, Is.Not.Null); var file = _packageFileService.FindFile("source\\file.txt", container); _packageFileService.MoveFile(container, file, "target"); // Old location gone - var oldNode = _viewModel.GetFromPath(root, "source\\file.txt"); + var oldNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "source\\file.txt"); Assert.That(oldNode, Is.Null, "File should not be in old folder after move"); // New location has the file - var targetFolder = _viewModel.GetFromPath(root, "target"); + var targetFolder = PackFileBrowserViewModelTestHelper.GetFromPath(root, "target"); targetFolder.IsNodeExpanded = true; - var newNode = _viewModel.GetFromPath(root, "target\\file.txt"); + var newNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "target\\file.txt"); Assert.That(newNode, Is.Not.Null, "File should appear in target folder after move"); } @@ -591,11 +591,11 @@ public void DragDrop_FileOntoFile_NotAllowed() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); folderA.IsNodeExpanded = true; - var file1 = _viewModel.GetFromPath(root, "foldera\\file1.txt"); - var file2 = _viewModel.GetFromPath(root, "foldera\\file2.txt"); + var file1 = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\file1.txt"); + var file2 = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\file2.txt"); Assert.That(_viewModel.AllowDrop(file1, file2), Is.False, "Should not allow dropping file onto file"); } @@ -607,8 +607,8 @@ public void DragDrop_FolderDrag_NotAllowed() var root = _viewModel.Files[0]; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); - var folderB = _viewModel.GetFromPath(root, "folderb"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); + var folderB = PackFileBrowserViewModelTestHelper.GetFromPath(root, "folderb"); Assert.That(_viewModel.AllowDrop(folderA, folderB), Is.False, "Should not allow dragging folders"); } @@ -622,14 +622,14 @@ public void ShowFoldersOnly_HidesFiles() _viewModel.Filter.ShowFoldersOnly = true; root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null, "Folder should be visible with ShowFoldersOnly"); Assert.That(folderA.IsVisible, Is.True); folderA.IsNodeExpanded = true; // File nodes are not materialized when ShowFoldersOnly is active - var fileNode = _viewModel.GetFromPath(root, "foldera\\file.txt"); + var fileNode = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\file.txt"); Assert.That(fileNode, Is.Null, "File should not be materialized with ShowFoldersOnly active"); // Only folder children should be present @@ -649,21 +649,21 @@ public void SearchAutoExpandsWhenResultCountBelowThreshold() var root = _viewModel.Files[0]; Assert.That(root.IsNodeExpanded, Is.False, "Root should start collapsed"); - // Filter matches all 3 files — well below the 25 threshold + // Filter matches all 3 files — well below the 25 threshold _viewModel.Filter.FilterText = "file"; // All matching nodes should be auto-expanded Assert.That(root.IsNodeExpanded, Is.True, "Root should be expanded when results < threshold"); - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); Assert.That(folderA.IsNodeExpanded, Is.True, "folderA should be auto-expanded"); - var sub = _viewModel.GetFromPath(root, "foldera\\sub"); + var sub = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub"); Assert.That(sub, Is.Not.Null); Assert.That(sub.IsNodeExpanded, Is.True, "sub should be auto-expanded"); - var folderB = _viewModel.GetFromPath(root, "folderb"); + var folderB = PackFileBrowserViewModelTestHelper.GetFromPath(root, "folderb"); Assert.That(folderB, Is.Not.Null); Assert.That(folderB.IsNodeExpanded, Is.True, "folderB should be auto-expanded"); } @@ -697,7 +697,7 @@ public void SearchDoesNotAutoExpandWhenResultCountAboveThreshold() public void DoubleClickCollapseAndReexpand_ChildrenRemainVisible() { // Scenario: user expands folders manually, collapses a parent, - // then re-expands it — child folders that were expanded should still show their children. + // then re-expands it — child folders that were expanded should still show their children. CreatePackfiles( ("folderA\\sub\\file1.txt", "file1.txt"), ("folderA\\sub\\file2.txt", "file2.txt")); @@ -706,15 +706,15 @@ public void DoubleClickCollapseAndReexpand_ChildrenRemainVisible() // Expand the tree manually root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); folderA.IsNodeExpanded = true; - var sub = _viewModel.GetFromPath(root, "foldera\\sub"); + var sub = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub"); Assert.That(sub, Is.Not.Null); sub.IsNodeExpanded = true; // Verify files are visible - var file1 = _viewModel.GetFromPath(root, "foldera\\sub\\file1.txt"); + var file1 = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub\\file1.txt"); Assert.That(file1, Is.Not.Null); // User double-clicks root to collapse @@ -726,16 +726,16 @@ public void DoubleClickCollapseAndReexpand_ChildrenRemainVisible() Assert.That(root.IsNodeExpanded, Is.True); // folderA should still be visible and expanded with its children materialized - folderA = _viewModel.GetFromPath(root, "foldera"); + folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null, "folderA should be visible after re-expanding root"); Assert.That(folderA.IsNodeExpanded, Is.True, "folderA should still be expanded"); - sub = _viewModel.GetFromPath(root, "foldera\\sub"); + sub = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub"); Assert.That(sub, Is.Not.Null, "sub should be visible since folderA is expanded"); Assert.That(sub.IsNodeExpanded, Is.True, "sub should still be expanded"); // Files inside sub should be materialized - file1 = _viewModel.GetFromPath(root, "foldera\\sub\\file1.txt"); + file1 = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub\\file1.txt"); Assert.That(file1, Is.Not.Null, "file1 should be visible inside expanded sub folder"); } @@ -747,7 +747,7 @@ public void OverwriteFileInCollapsedFolder_NoDuplicateNodeInTree() var root = _viewModel.Files[0]; var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); - // Folder is collapsed — BackingChildren are NOT yet loaded. + // Folder is collapsed — BackingChildren are NOT yet loaded. Assert.That(root.IsNodeExpanded, Is.False); // Act: add a second file with the same name (simulates Save As / overwrite import). @@ -756,7 +756,7 @@ public void OverwriteFileInCollapsedFolder_NoDuplicateNodeInTree() // Expand to materialise children and assert only ONE node with that name exists. root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); folderA.IsNodeExpanded = true; @@ -777,7 +777,7 @@ public void OverwriteFileInExpandedFolder_NoDuplicateNodeInTree() var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); root.IsNodeExpanded = true; - var folderA = _viewModel.GetFromPath(root, "foldera"); + var folderA = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera"); Assert.That(folderA, Is.Not.Null); folderA.IsNodeExpanded = true; @@ -797,3 +797,4 @@ public void OverwriteFileInExpandedFolder_NoDuplicateNodeInTree() } } + From 115f32b446dd6b3d147fa17903b27e20f34b9dc5 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 11:30:12 +0200 Subject: [PATCH 14/26] Code cleanup --- .../Shared.Core/Services/IStandardDialogs.cs | 2 + .../Commands/CreateFolderCommand.cs | 15 ++- .../PackFileTree/EditFileNameDialog.cs | 94 ------------------- .../PackFileTree/PackFileBrowserViewModel.cs | 1 + .../BaseDialogs/PackFileTree/SearchFilter.cs | 10 +- .../BaseDialogs/PackFileTree/TreeNode.cs | 9 ++ .../TreeNodeManipulationHelper.cs | 0 .../{ => Utility}/TreeNodePathHelper.cs | 0 .../{ => Utility}/TreeNodeStateHelper.cs | 0 .../StandardDialog/StandardDialogs.cs | 52 ++++++++++ 10 files changed, 80 insertions(+), 103 deletions(-) delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/EditFileNameDialog.cs rename Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/{ => Utility}/TreeNodeManipulationHelper.cs (100%) rename Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/{ => Utility}/TreeNodePathHelper.cs (100%) rename Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/{ => Utility}/TreeNodeStateHelper.cs (100%) diff --git a/Shared/SharedCore/Shared.Core/Services/IStandardDialogs.cs b/Shared/SharedCore/Shared.Core/Services/IStandardDialogs.cs index b9683c105..7fe09ca19 100644 --- a/Shared/SharedCore/Shared.Core/Services/IStandardDialogs.cs +++ b/Shared/SharedCore/Shared.Core/Services/IStandardDialogs.cs @@ -1,6 +1,7 @@ using Shared.Core.ErrorHandling; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; +using System.Collections.Generic; namespace Shared.Core.Services { @@ -9,6 +10,7 @@ public interface IStandardDialogs SaveDialogResult DisplaySaveDialog(IPackFileService pfs, List extensions); BrowseDialogResultFile DisplayBrowseDialog(List extensions); BrowseDialogResultFolder DisplayBrowseFolderDialog(); + string ShowFolderNameDialog(IEnumerable existingNames, string currentValue = ""); void ShowExceptionWindow(Exception e, string userInfo = ""); void ShowErrorViewDialog(string title, ErrorList errorItems, bool modal = true); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommand.cs index dbd4b8b17..7bbb9ad27 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommand.cs @@ -1,26 +1,25 @@ using System.Linq; -using System.Windows; -using Shared.Core.PackFiles; +using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class CreateFolderCommand(IPackFileService packFileService) : IContextMenuCommand + public class CreateFolderCommand(IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Create Folder"; public bool IsEnabled(TreeNode node) => true; - public void Execute(TreeNode _selectedNode) + public void Execute(TreeNode selectedNode) { - if (_selectedNode.FileOwner.IsCaPackFile) + if (selectedNode.FileOwner.IsCaPackFile) { - MessageBox.Show("Unable to edit CA packfile"); + standardDialogs.ShowDialogBox("Unable to edit CA packfile"); return; } - var folderName = EditFileNameDialog.ShowDialog(_selectedNode, ""); + var folderName = standardDialogs.ShowFolderNameDialog(selectedNode.Children.Select(x => x.Name), ""); if (folderName.Any()) - _selectedNode.AddDirectoryChild(folderName); + selectedNode.AddDirectoryChild(folderName); } } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/EditFileNameDialog.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/EditFileNameDialog.cs deleted file mode 100644 index fd79fe496..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/EditFileNameDialog.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.IO; -using CommonControls.BaseDialogs; -using MessageBox = System.Windows.MessageBox; - -namespace Shared.Ui.BaseDialogs.PackFileTree -{ - /// - /// Shows simple value entry dialog, for file/folder names - /// Shows error boxes on invalid input, asks the user to retry, and shows the dialog again - /// TODO: Make neater solution? Edit-in-place; make own simple value editing dialog, with checking - /// - public class EditFileNameDialog - { - /// - /// Shows dialog for entering a new folder name - /// - /// Parent Node for checking for folders with identical names - /// The Value of the property to edit - /// Edit Name, empty string if user pressed "cancel" - static public string ShowDialog(TreeNode parentNode, string currentValue) - { - var isInputCorrect = false; - var dialogTextBoxValue = currentValue; - var newFileName = ""; - - while (!isInputCorrect) - { - isInputCorrect = true; - var window = new TextInputWindow("Create folder", dialogTextBoxValue, true); - - if (window.ShowDialog() == false) - return ""; // exit if "cancel" pressed - - dialogTextBoxValue = window.TextValue; // store for next dialog instance (if ínput is invalud) - - isInputCorrect = isInputCorrect && IsStringProper(window.TextValue); - - newFileName = window.TextValue.ToLower().Trim(); - - isInputCorrect = isInputCorrect && IsFileNameUniqueInFolder(parentNode, newFileName); - - isInputCorrect = isInputCorrect && AreStringCharsValidForFileName(newFileName); - } - - return newFileName; - } - - private static bool IsStringProper(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - MessageBox.Show("Folder name can not be empty! Please Try Again.\nPlease Try Again."); - return false; - } - return true; - } - - private static bool IsFileNameUniqueInFolder(TreeNode parentNode, string newFileName) - { - if (parentNode == null) - throw new ArgumentNullException(nameof(parentNode), "Parent node can not be null!"); - - foreach (var node in parentNode.Children) - { - if (node.Name == newFileName) - { - MessageBox.Show($"Folder with name '{node.Name}' already exists in this folder!\nPlease Try Again."); - return false; - } - } - - return true; - } - - static private bool AreStringCharsValidForFileName(string fileNameToCheck) - { - var listOfInvalidChars = Path.GetInvalidFileNameChars(); - foreach (var c in fileNameToCheck) - { - foreach (var invalidChar in listOfInvalidChars) - { - if (c == invalidChar) - { - MessageBox.Show($"Folder name contains invalid character: {c}. \nPlease Try Again."); - return false; - } - } - } - - return true; - } - } -} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs index 39b94158b..c4671a2e3 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs @@ -77,6 +77,7 @@ public PackFileBrowserViewModel(ApplicationSettingsService applicationSettingsSe public void Dispose() { _eventHub?.UnRegister(this); + Filter?.Dispose(); } partial void OnSelectedItemChanged(TreeNode value) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/SearchFilter.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/SearchFilter.cs index a6d8f1012..de2fca6ec 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/SearchFilter.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/SearchFilter.cs @@ -10,7 +10,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree { - public class SearchFilter : NotifyPropertyChangedImpl, IDataErrorInfo + public class SearchFilter : NotifyPropertyChangedImpl, IDataErrorInfo, IDisposable { public string Error { get; set; } = string.Empty; public string this[string columnName] => ApplyFilter(FilterText); @@ -68,6 +68,7 @@ internal SearchFilter(ObservableCollection nodes, Func func) @@ -367,9 +371,14 @@ partial void OnIsNodeExpandedChanged(bool value) private void UnloadChildren() { + // Recursively unload all children to break closure cycles foreach (var child in Children) child.RemoveSelf(); + // Also unload any backing children that weren't materialized + foreach (var child in BackingChildren.Where(c => !Children.Contains(c))) + child.RemoveSelf(); + Children.Clear(); if (ShouldUsePlaceholder()) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeManipulationHelper.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/Utility/TreeNodeManipulationHelper.cs similarity index 100% rename from Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeManipulationHelper.cs rename to Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/Utility/TreeNodeManipulationHelper.cs diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodePathHelper.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/Utility/TreeNodePathHelper.cs similarity index 100% rename from Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodePathHelper.cs rename to Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/Utility/TreeNodePathHelper.cs diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeStateHelper.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/Utility/TreeNodeStateHelper.cs similarity index 100% rename from Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeStateHelper.cs rename to Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/Utility/TreeNodeStateHelper.cs diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/StandardDialogs.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/StandardDialogs.cs index 9ee375ac8..9ef6ad4c7 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/StandardDialogs.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/StandardDialogs.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Windows; using CommonControls.BaseDialogs; using CommonControls.BaseDialogs.ErrorListDialog; @@ -89,6 +91,56 @@ public BrowseDialogResultFolder DisplayBrowseFolderDialog() return output; } + public string ShowFolderNameDialog(IEnumerable existingNames, string currentValue = "") + { + var isInputCorrect = false; + var dialogTextBoxValue = currentValue; + var normalizedExistingNames = new HashSet(existingNames.Select(x => x.ToLower().Trim())); + + while (!isInputCorrect) + { + isInputCorrect = true; + var window = new TextInputWindow("Create folder", dialogTextBoxValue, true); + + if (window.ShowDialog() == false) + return string.Empty; + + dialogTextBoxValue = window.TextValue; + var newFolderName = window.TextValue.ToLower().Trim(); + + if (string.IsNullOrWhiteSpace(newFolderName)) + { + MessageBox.Show("Folder name can not be empty! Please Try Again."); + isInputCorrect = false; + } + + if (isInputCorrect && normalizedExistingNames.Contains(newFolderName)) + { + MessageBox.Show($"Folder with name '{newFolderName}' already exists in this folder!\nPlease Try Again."); + isInputCorrect = false; + } + + if (isInputCorrect) + { + var listOfInvalidChars = Path.GetInvalidFileNameChars(); + foreach (var c in newFolderName) + { + if (listOfInvalidChars.Contains(c)) + { + MessageBox.Show($"Folder name contains invalid character: {c}. \nPlease Try Again."); + isInputCorrect = false; + break; + } + } + } + + if (isInputCorrect) + return newFolderName; + } + + return string.Empty; + } + public void ShowExceptionWindow(Exception e, string userInfo = "") { var extendedException = _exceptionService.Create(e); From b34c90514b72f918f7c1f17a568ca3ca2e8cf75c Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 11:36:34 +0200 Subject: [PATCH 15/26] Code --- .../ContextMenu/MainApplicationContextMenuBuilder.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs index 0fff2b7a1..90eaf32d4 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs @@ -5,13 +5,6 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu { - - - - - - - public class MainApplicationContextMenuBuilder : ContextMenuBuilder { private readonly IPackFileService _packFileService; From 2f9f215b8b31d328dd9e1dc3fc0e6c966833656b Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 12:35:07 +0200 Subject: [PATCH 16/26] Update to context menu system --- .../IExportFileContextMenuHelper.cs | 6 +- .../IImportFileContextMenuHelper.cs | 11 ++ .../DependencyInjectionContainer.cs | 16 +- .../Exporting/AdvancedExportCommand.cs | 15 ++ .../Exporting/ExportFileContextMenuHelper.cs | 2 +- .../Importing/AdvancedImportCommand.cs | 15 ++ .../Importing/ImportFIleContextMenuHelper.cs | 2 +- .../UiCommands/QuickExportPosedMeshCommand.cs | 2 +- .../Reports/DependencyInjectionContainer.cs | 12 +- Editors/Reports/Geometry/RmvToTextReport.cs | 6 +- .../Commands/AdvancedExportCommand.cs | 12 -- .../Commands/AdvancedImportCommand.cs | 12 -- .../Commands/ClosePackContainerFileCommand.cs | 1 + .../Commands/CollapseNodeCommand.cs | 1 + .../Commands/CopyNodePathCommand.cs | 1 + .../Commands/CopyToEditablePackCommand.cs | 7 +- .../Commands/CreateFolderCommand.cs | 1 + .../ContextMenu/Commands/DeleteNodeCommand.cs | 1 + .../Commands/DuplicateFileCommand.cs | 1 + .../ContextMenu/Commands/ExpandNodeCommand.cs | 1 + .../Commands/ExportToDirectoryCommand.cs | 1 + .../Commands/IContextMenuCommand.cs | 1 + .../Commands/ImportDirectoryCommand.cs | 1 + .../Commands/ImportFilesCommand.cs | 1 + .../ContextMenu/Commands/OpenNodeInCommand.cs | 1 + .../Commands/OpenPackInFileExplorerCommand.cs | 1 + ...ameNodeCommand.cs => RenameNodeCommand.cs} | 3 +- .../SaveAsPackFileContainerCommand.cs | 1 + .../Commands/SavePackFileContainerCommand.cs | 1 + .../Commands/SetAsEditablePackCommand.cs | 1 + .../ContextMenu/ContextMenuBuilder.cs | 164 +++++++++++++----- .../External/IImportFileContextMenuHelper.cs | 17 -- .../ContextMenu/External/IRmvToTextCommand.cs | 8 - .../MainApplicationContextMenuBuilder.cs | 114 ------------ .../ContextMenu/NoContextMenuBuilder.cs | 14 -- .../ContextMenu/SimpleContextMenuBuilder.cs | 24 --- .../PackFileTree/PackFileBrowserViewModel.cs | 10 +- .../PackFileTree/PackFileTreeViewFactory.cs | 9 +- .../Shared.Ui/DependencyInjectionContainer.cs | 54 +++++- 39 files changed, 276 insertions(+), 275 deletions(-) rename {Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External => Editors/ImportExportEditor/Editors.ImportExport/ContextMenu}/IExportFileContextMenuHelper.cs (59%) create mode 100644 Editors/ImportExportEditor/Editors.ImportExport/ContextMenu/IImportFileContextMenuHelper.cs create mode 100644 Editors/ImportExportEditor/Editors.ImportExport/Exporting/AdvancedExportCommand.cs create mode 100644 Editors/ImportExportEditor/Editors.ImportExport/Importing/AdvancedImportCommand.cs delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/AdvancedExportCommand.cs delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/AdvancedImportCommand.cs rename Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/{OnRenameNodeCommand.cs => RenameNodeCommand.cs} (85%) delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IImportFileContextMenuHelper.cs delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IRmvToTextCommand.cs delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/NoContextMenuBuilder.cs delete mode 100644 Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/SimpleContextMenuBuilder.cs diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IExportFileContextMenuHelper.cs b/Editors/ImportExportEditor/Editors.ImportExport/ContextMenu/IExportFileContextMenuHelper.cs similarity index 59% rename from Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IExportFileContextMenuHelper.cs rename to Editors/ImportExportEditor/Editors.ImportExport/ContextMenu/IExportFileContextMenuHelper.cs index df26eb697..b76e73779 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IExportFileContextMenuHelper.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/ContextMenu/IExportFileContextMenuHelper.cs @@ -1,10 +1,10 @@ -using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models; -namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External +namespace Editors.ImportExport.ContextMenu { public interface IExportFileContextMenuHelper { bool CanExportFile(PackFile packFile); void ShowDialog(PackFile packFile); } -} +} \ No newline at end of file diff --git a/Editors/ImportExportEditor/Editors.ImportExport/ContextMenu/IImportFileContextMenuHelper.cs b/Editors/ImportExportEditor/Editors.ImportExport/ContextMenu/IImportFileContextMenuHelper.cs new file mode 100644 index 000000000..45b840eca --- /dev/null +++ b/Editors/ImportExportEditor/Editors.ImportExport/ContextMenu/IImportFileContextMenuHelper.cs @@ -0,0 +1,11 @@ +using Shared.Core.PackFiles.Models; +using TreeNode = Shared.Ui.BaseDialogs.PackFileTree.TreeNode; + +namespace Editors.ImportExport.ContextMenu +{ + public interface IImportFileContextMenuHelper + { + bool CanImportFile(PackFile file); + void ShowDialog(TreeNode node); + } +} \ No newline at end of file diff --git a/Editors/ImportExportEditor/Editors.ImportExport/DependencyInjectionContainer.cs b/Editors/ImportExportEditor/Editors.ImportExport/DependencyInjectionContainer.cs index 5951f30bc..5e768febc 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/DependencyInjectionContainer.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/DependencyInjectionContainer.cs @@ -10,6 +10,7 @@ using Editors.ImportExport.Exporting.Presentation.DdsToNormalPng; using Editors.ImportExport.Exporting.Presentation.DdsToPng; using Editors.ImportExport.Exporting.Presentation.RmvToGltf; +using Editors.ImportExport.ContextMenu; using Editors.ImportExport.Importing; using Editors.ImportExport.Importing.Importers.GltfToRmv; using Editors.ImportExport.Importing.Importers.GltfToRmv.Helper; @@ -18,7 +19,7 @@ using Shared.Core.DependencyInjection; using Shared.Core.DevConfig; using Editors.ImportImport.Importing.Presentation.RmvToGltf; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu; namespace Editors.ImportExport { @@ -55,6 +56,8 @@ public override void Register(IServiceCollection services) services.AddScoped(); // Helpers to ensure we can hook up to the UI + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -69,7 +72,18 @@ public override void Register(IServiceCollection services) services.AddTransient(); services.AddTransient(); + services.AddSingleton(); + RegisterAllAsInterface(services, ServiceLifetime.Transient); } } + + public class ImportExportPackFileContextMenuRegistration : IPackFileContextMenuRegistration + { + public void Register(PackFileContextMenuRegistry registry) + { + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "Export", priority: 10, ContextMenuCluster.Export); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "Import", priority: 20, ContextMenuCluster.FolderOperation); + } + } } diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Exporting/AdvancedExportCommand.cs b/Editors/ImportExportEditor/Editors.ImportExport/Exporting/AdvancedExportCommand.cs new file mode 100644 index 000000000..c9ba6e577 --- /dev/null +++ b/Editors/ImportExportEditor/Editors.ImportExport/Exporting/AdvancedExportCommand.cs @@ -0,0 +1,15 @@ +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; +using Shared.Ui.BaseDialogs.PackFileTree; +using Editors.ImportExport.ContextMenu; + +namespace Editors.ImportExport.Exporting +{ + public class AdvancedExportCommand(IExportFileContextMenuHelper exportFileContextMenuHelper) : IContextMenuCommand + { + public string GetDisplayName(TreeNode node) => "Advanced Export"; + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.File && node.Item != null; + public bool IsEnabled(TreeNode node) => exportFileContextMenuHelper.CanExportFile(node.Item); + + public void Execute(TreeNode selectedNode) => exportFileContextMenuHelper.ShowDialog(selectedNode.Item); + } +} diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Exporting/ExportFileContextMenuHelper.cs b/Editors/ImportExportEditor/Editors.ImportExport/Exporting/ExportFileContextMenuHelper.cs index 458514904..6773ff96a 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/Exporting/ExportFileContextMenuHelper.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/Exporting/ExportFileContextMenuHelper.cs @@ -1,9 +1,9 @@ using Editors.ImportExport.Misc; using Editors.ImportExport.Exporting.Exporters; +using Editors.ImportExport.ContextMenu; using Shared.Core.Events; using Shared.Core.PackFiles.Models; using Shared.Core.Settings; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; namespace Editors.ImportExport.Exporting { diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Importing/AdvancedImportCommand.cs b/Editors/ImportExportEditor/Editors.ImportExport/Importing/AdvancedImportCommand.cs new file mode 100644 index 000000000..79c5c8e48 --- /dev/null +++ b/Editors/ImportExportEditor/Editors.ImportExport/Importing/AdvancedImportCommand.cs @@ -0,0 +1,15 @@ +using Editors.ImportExport.ContextMenu; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Editors.ImportExport.Importing +{ + public class AdvancedImportCommand(IImportFileContextMenuHelper importFileContextMenuHelper) : IContextMenuCommand + { + public string GetDisplayName(TreeNode node) => "Advanced Import"; + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Directory && !node.FileOwner.IsCaPackFile; + public bool IsEnabled(TreeNode node) => true; + + public void Execute(TreeNode selectedNode) => importFileContextMenuHelper.ShowDialog(selectedNode); + } +} diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Importing/ImportFIleContextMenuHelper.cs b/Editors/ImportExportEditor/Editors.ImportExport/Importing/ImportFIleContextMenuHelper.cs index 01c671571..600fcab1e 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/Importing/ImportFIleContextMenuHelper.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/Importing/ImportFIleContextMenuHelper.cs @@ -1,10 +1,10 @@ using System.IO; +using Editors.ImportExport.ContextMenu; using Editors.ImportExport.Exporting.Exporters; using Editors.ImportExport.Misc; using Shared.Core.Events; using Shared.Core.PackFiles.Models; using Shared.Core.Settings; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; using TreeNode = Shared.Ui.BaseDialogs.PackFileTree.TreeNode; namespace Editors.ImportExport.Importing diff --git a/Editors/Kitbashing/KitbasherEditor/UiCommands/QuickExportPosedMeshCommand.cs b/Editors/Kitbashing/KitbasherEditor/UiCommands/QuickExportPosedMeshCommand.cs index 879432ade..7ee4fa44e 100644 --- a/Editors/Kitbashing/KitbasherEditor/UiCommands/QuickExportPosedMeshCommand.cs +++ b/Editors/Kitbashing/KitbasherEditor/UiCommands/QuickExportPosedMeshCommand.cs @@ -13,9 +13,9 @@ using Shared.Core.Misc; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; +using Editors.ImportExport.ContextMenu; using Shared.Ui.Common.MenuSystem; using Shared.GameFormats.RigidModel; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; // <- added using MessageBox = System.Windows.MessageBox; namespace Editors.KitbasherEditor.UiCommands diff --git a/Editors/Reports/DependencyInjectionContainer.cs b/Editors/Reports/DependencyInjectionContainer.cs index edd16d4d7..aa0b2882d 100644 --- a/Editors/Reports/DependencyInjectionContainer.cs +++ b/Editors/Reports/DependencyInjectionContainer.cs @@ -6,7 +6,7 @@ using Editors.Reports.Geometry; using Microsoft.Extensions.DependencyInjection; using Shared.Core.DependencyInjection; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu; namespace Editors.Reports { @@ -40,8 +40,8 @@ public override void Register(IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); - serviceCollection.AddScoped(); serviceCollection.AddTransient(); + serviceCollection.AddSingleton(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); @@ -53,4 +53,12 @@ public override void Register(IServiceCollection serviceCollection) serviceCollection.AddTransient(); } } + + public class ReportsPackFileContextMenuRegistration : IPackFileContextMenuRegistration + { + public void Register(PackFileContextMenuRegistry registry) + { + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "Reports", priority: 50, ContextMenuCluster.Reports); + } + } } diff --git a/Editors/Reports/Geometry/RmvToTextReport.cs b/Editors/Reports/Geometry/RmvToTextReport.cs index 2bcae0249..969ec7ca0 100644 --- a/Editors/Reports/Geometry/RmvToTextReport.cs +++ b/Editors/Reports/Geometry/RmvToTextReport.cs @@ -7,14 +7,16 @@ using Shared.GameFormats.RigidModel.LodHeader; using Shared.GameFormats.RigidModel.MaterialHeaders; using Shared.Ui.BaseDialogs.PackFileTree; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; namespace Editors.Reports.Geometry { - public class RmvToTextCommand(RmvToTextReport report) : IRmvToTextCommand + public class RmvToTextCommand(RmvToTextReport report) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Generate Rmv to Text"; + public bool ShouldAdd(TreeNode node) => IsEnabled(node); + public bool IsEnabled(TreeNode node) => node.NodeType == NodeType.File && node.Item != null && node.Name.EndsWith(".rigid_model_v2", StringComparison.OrdinalIgnoreCase); public void Execute(TreeNode node) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/AdvancedExportCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/AdvancedExportCommand.cs deleted file mode 100644 index fe88472fd..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/AdvancedExportCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; - -namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands -{ - public class AdvancedExportCommand(IExportFileContextMenuHelper exportFileContextMenuHelper) : IContextMenuCommand - { - public string GetDisplayName(TreeNode node) => "Advanced Export"; - public bool IsEnabled(TreeNode node) => exportFileContextMenuHelper.CanExportFile(node.Item); - - public void Execute(TreeNode _selectedNode) => exportFileContextMenuHelper.ShowDialog(_selectedNode.Item); - } -} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/AdvancedImportCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/AdvancedImportCommand.cs deleted file mode 100644 index 1e31b4aeb..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/AdvancedImportCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; - -namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands -{ - public class AdvancedImportCommand(IImportFileContextMenuHelper importFileContextMenuHelper) : IContextMenuCommand - { - public string GetDisplayName(TreeNode node) => "Advanced Import"; - public bool IsEnabled(TreeNode node) => true; - - public void Execute(TreeNode _selectedNode) => importFileContextMenuHelper.ShowDialog(_selectedNode); - } -} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommand.cs index 09f515679..f6dfd46ed 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommand.cs @@ -6,6 +6,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class ClosePackContainerFileCommand(IPackFileService packFileService) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Close"; + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Root; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommand.cs index e08dda2e2..43cd779a7 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommand.cs @@ -3,6 +3,7 @@ public class CollapseNodeCommand() : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Collapse all"; + public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) => CollapsAllRecursive(_selectedNode); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyNodePathCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyNodePathCommand.cs index 912d1065c..fe165f89a 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyNodePathCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyNodePathCommand.cs @@ -6,6 +6,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class CopyNodePathCommand(IPackFileService packFileService) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Copy full path"; + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.File && node.Item != null; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommand.cs index 497ab0b80..008230315 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommand.cs @@ -9,6 +9,11 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class CopyToEditablePackCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Copy to editable pack"; + public bool ShouldAdd(TreeNode node) + { + var editablePack = packFileService.GetEditablePack(); + return editablePack != null && editablePack != node.FileOwner; + } public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) @@ -27,6 +32,4 @@ public void Execute(TreeNode _selectedNode) } } } - - } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommand.cs index 7bbb9ad27..6c0642da6 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommand.cs @@ -6,6 +6,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class CreateFolderCommand(IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Create Folder"; + public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DeleteNodeCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DeleteNodeCommand.cs index 14a5ef1f1..f72dd7db2 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DeleteNodeCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DeleteNodeCommand.cs @@ -6,6 +6,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class DeleteNodeCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Delete"; + public bool ShouldAdd(TreeNode node) => (node.NodeType == NodeType.File || node.NodeType == NodeType.Directory) && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommand.cs index f9863a797..5c6dbebfc 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommand.cs @@ -8,6 +8,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class DuplicateFileCommand(IPackFileService packFileService) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Duplicate"; + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.File && node.Item != null && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) => Execute(_selectedNode.Item); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommand.cs index dfcfb5bae..2a938760c 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommand.cs @@ -3,6 +3,7 @@ public class ExpandNodeCommand() : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Expand all"; + public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) => ExpandAllRecursive(_selectedNode); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs index 3cecbea6d..013d2aebc 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs @@ -7,6 +7,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class ExportToDirectoryCommand() : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Export to system folder"; + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Directory || node.NodeType == NodeType.Root || (node.NodeType == NodeType.File && node.Item != null); public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/IContextMenuCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/IContextMenuCommand.cs index 5ed5d20ba..2e9a56a17 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/IContextMenuCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/IContextMenuCommand.cs @@ -5,6 +5,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public interface IContextMenuCommand : IUiCommand { public string GetDisplayName(TreeNode node); + public bool ShouldAdd(TreeNode node); public bool IsEnabled(TreeNode node); public void Execute(TreeNode node); } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs index 605674eb5..2718518d0 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs @@ -11,6 +11,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class ImportDirectoryCommand(IPackFileService packFileService) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Import Directory"; + public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs index 2ae1cfbf5..2d103eeb3 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs @@ -9,6 +9,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class ImportFileCommand(IPackFileService packFileService) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Import File"; + public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs index f8159cfec..389a8c99d 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs @@ -9,6 +9,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public abstract class OpenNodeInCommand() : IContextMenuCommand { public abstract string GetDisplayName(TreeNode node); + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.File && node.Item != null; public bool IsEnabled(TreeNode node) => true; public abstract void Execute(TreeNode _selectedNode); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs index 4aed7cd9e..5e25f5fef 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs @@ -7,6 +7,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class OpenPackInFileExplorerCommand(IPackFileService packFileService) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Open In File Explorer"; + public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OnRenameNodeCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/RenameNodeCommand.cs similarity index 85% rename from Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OnRenameNodeCommand.cs rename to Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/RenameNodeCommand.cs index 063f9beee..11a592387 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OnRenameNodeCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/RenameNodeCommand.cs @@ -4,9 +4,10 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class OnRenameNodeCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand + public class RenameNodeCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Rename"; + public bool ShouldAdd(TreeNode node) => (node.NodeType == NodeType.File || node.NodeType == NodeType.Directory) && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs index c52244665..4cf02d096 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs @@ -8,6 +8,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class SaveAsPackFileContainerCommand(IPackFileService packFileService, ApplicationSettingsService applicationSettingsService) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Save As"; + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Root && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs index fb5af6391..2d465de83 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs @@ -16,6 +16,7 @@ public class SavePackFileContainerCommand( { private readonly ILogger _logger = Logging.Create(); public string GetDisplayName(TreeNode node) => "Save"; + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Root && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommand.cs index fc25a80e4..c18f7cf9e 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommand.cs @@ -5,6 +5,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands public class SetAsEditablePackCommand(IPackFileService packFileService) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Set as Editable Pack"; + public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Root && !node.FileOwner.IsCaPackFile && packFileService.GetEditablePack() != node.FileOwner; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode selectedNode) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuBuilder.cs index 21ffc89c9..1946e5488 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuBuilder.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/ContextMenuBuilder.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using Shared.Core.Events; +using Microsoft.Extensions.DependencyInjection; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu @@ -14,77 +15,156 @@ public enum ContextMenuType SceneExplorer } - public class ContextMenuFactory + public enum ContextMenuCluster { - private readonly IEnumerable _builders; - - public ContextMenuFactory(IEnumerable builders) - { - _builders = builders; - } + PackFileOperation, + FolderOperation, + FileOperation, + Export, + Reports, + Misc + } - public IContextMenuBuilder GetContextMenu(ContextMenuType menuType) => _builders.First(x => x.Type == menuType); + public interface IPackFileContextMenuRegistration + { + void Register(PackFileContextMenuRegistry registry); } - public interface IContextMenuBuilder + public sealed class PackFileContextMenuItemRegistration { - ContextMenuType Type { get; } - public ObservableCollection Build(TreeNode? node); + public required Type CommandType { get; init; } + public required ContextMenuType ContextMenuType { get; init; } + public required ContextMenuCluster Cluster { get; init; } + public required string Path { get; init; } + public required int Priority { get; init; } } - public abstract class ContextMenuBuilder : IContextMenuBuilder + public class PackFileContextMenuRegistry { - private readonly IUiCommandFactory _commandFactory; + private readonly List _items = []; - public ContextMenuType Type { get; private set; } + public void RegisterPackFileContextMenuItem(ContextMenuType contextMenuType, string path, int priority, ContextMenuCluster cluster) + where TCommand : IContextMenuCommand + { + RegisterPackFileContextMenuItem(typeof(TCommand), contextMenuType, path, priority, cluster); + } - public ContextMenuBuilder(ContextMenuType type, IUiCommandFactory commandFactory) + public void RegisterPackFileContextMenuItem(Type commandType, ContextMenuType contextMenuType, string path, int priority, ContextMenuCluster cluster) { - Type = type; - _commandFactory = commandFactory; + if (!typeof(IContextMenuCommand).IsAssignableFrom(commandType)) + throw new System.ArgumentException($"{commandType.Name} must implement {nameof(IContextMenuCommand)}"); + + _items.Add(new PackFileContextMenuItemRegistration + { + CommandType = commandType, + ContextMenuType = contextMenuType, + Cluster = cluster, + Path = path ?? string.Empty, + Priority = priority + }); } - protected abstract void Create(ContextMenuItem rootNode, TreeNode selectedNode); + public IReadOnlyList Get(ContextMenuType contextMenuType) + { + return _items + .Where(x => x.ContextMenuType == contextMenuType) + .OrderBy(x => x.Cluster) + .ThenBy(x => x.Priority) + .ThenBy(x => x.Path) + .ThenBy(x => x.CommandType.FullName) + .ToList(); + } + } + + public class PackFileContextMenuComposer + { + private readonly PackFileContextMenuRegistry _registry; + private readonly IServiceProvider _serviceProvider; + + public PackFileContextMenuComposer(PackFileContextMenuRegistry registry, IServiceProvider serviceProvider) + { + _registry = registry; + _serviceProvider = serviceProvider; + } - public ObservableCollection Build(TreeNode? node) + public ObservableCollection Build(ContextMenuType contextMenuType, TreeNode? node) { var output = new ObservableCollection(); if (node == null) return output; - var placeholderRoot = new ContextMenuItem("Root", null); - Create(placeholderRoot, node); + var items = _registry.Get(contextMenuType); + var hasClusterContent = false; - foreach (var item in placeholderRoot.ContextMenu) - output.Add(item); + foreach (var cluster in System.Enum.GetValues()) + { + var clusterRoot = new ContextMenuItem("Root", null); + var pathToMenuLookup = new Dictionary(System.StringComparer.OrdinalIgnoreCase); - return output; - } + foreach (var item in items.Where(x => x.Cluster == cluster)) + { + var command = (IContextMenuCommand)_serviceProvider.GetRequiredService(item.CommandType); + if (!command.ShouldAdd(node) || !command.IsEnabled(node)) + continue; + var parent = GetOrCreateMenuPath(item.Path, clusterRoot, pathToMenuLookup); + parent.ContextMenu.Add(new ContextMenuItem(command.GetDisplayName(node), () => command.Execute(node))); + } - protected void Add(TreeNode? node, ContextMenuItem parent) where T : IContextMenuCommand - { - var instance = _commandFactory.Create(); + RemoveEmptySubmenus(clusterRoot); + if (clusterRoot.ContextMenu.Count == 0) + continue; + + if (hasClusterContent) + output.Add(null); + + foreach (var menuItem in clusterRoot.ContextMenu) + output.Add(menuItem); - if (instance.IsEnabled(node) == false) - return; + hasClusterContent = true; + } - var name = instance.GetDisplayName(node); - var item = new ContextMenuItem(name, () => instance.Execute(node)); - parent.ContextMenu.Add(item); + return output; } - protected void AddSeperator(ContextMenuItem parent) + private static ContextMenuItem GetOrCreateMenuPath(string path, ContextMenuItem root, Dictionary pathToMenuLookup) { - parent.ContextMenu.Add(null); + if (string.IsNullOrWhiteSpace(path)) + return root; + + var current = root; + var runningPath = string.Empty; + foreach (var pathSegment in path.Split('/').Where(x => !string.IsNullOrWhiteSpace(x))) + { + runningPath = string.IsNullOrEmpty(runningPath) ? pathSegment : $"{runningPath}/{pathSegment}"; + + if (pathToMenuLookup.TryGetValue(runningPath, out var menu) == false) + { + menu = new ContextMenuItem(pathSegment, null); + current.ContextMenu.Add(menu); + pathToMenuLookup[runningPath] = menu; + } + + current = menu; + } + + return current; } - public ContextMenuItem AddChildMenu(string name, ContextMenuItem parent) + private static void RemoveEmptySubmenus(ContextMenuItem root) { - var newItem = new ContextMenuItem(name, null); - parent.ContextMenu.Add(newItem); - return newItem; - + for (var i = root.ContextMenu.Count - 1; i >= 0; i--) + { + var menuItem = root.ContextMenu[i]; + if (menuItem == null) + continue; + + if (menuItem.ContextMenu.Count > 0) + RemoveEmptySubmenus(menuItem); + + if (menuItem.Command == null && menuItem.ContextMenu.Count == 0) + root.ContextMenu.RemoveAt(i); + } } } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IImportFileContextMenuHelper.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IImportFileContextMenuHelper.cs deleted file mode 100644 index 667c9f6f5..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IImportFileContextMenuHelper.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Shared.Core.PackFiles.Models; - -namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External -{ - public interface IImportFileContextMenuHelper - { - /// - /// Implemeation checks if the can be file imported - /// - /// - /// - bool CanImportFile(PackFile file); - - // implemenation returns packfile container, so it can create new foldders - public void ShowDialog(TreeNode node); - } -} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IRmvToTextCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IRmvToTextCommand.cs deleted file mode 100644 index 4a0555025..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/External/IRmvToTextCommand.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; - -namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External -{ - public interface IRmvToTextCommand : IContextMenuCommand - { - } -} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs deleted file mode 100644 index 90eaf32d4..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/MainApplicationContextMenuBuilder.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Shared.Core.Events; -using Shared.Core.PackFiles; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.External; - -namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu -{ - public class MainApplicationContextMenuBuilder : ContextMenuBuilder - { - private readonly IPackFileService _packFileService; - - public MainApplicationContextMenuBuilder(IPackFileService packFileService, IUiCommandFactory commandFactory) : base(ContextMenuType.MainApplication, commandFactory) - { - _packFileService = packFileService; - } - - protected override void Create(ContextMenuItem rootNode, TreeNode selectedNode) - { - var nodeType = selectedNode.NodeType; - switch (nodeType) - { - case NodeType.File: - CreateForFile(rootNode, selectedNode); - break; - case NodeType.Root: - CreateForDirectory(rootNode, selectedNode); - break; - case NodeType.Directory: - CreateForDirectory(rootNode, selectedNode); - break; - } - } - - void CreateForFile(ContextMenuItem rootNode, TreeNode selectedNode) - { - if (_packFileService.GetEditablePack() != selectedNode.FileOwner) - Add(selectedNode, rootNode); - - if (!selectedNode.FileOwner.IsCaPackFile) - { - AddSeperator(rootNode); - Add(selectedNode, rootNode); - Add(selectedNode, rootNode); - Add(selectedNode, rootNode); - AddSeperator(rootNode); - } - - Add(selectedNode, rootNode); - - var exportFolder = AddChildMenu("Export", rootNode); - Add(selectedNode, exportFolder); - Add(selectedNode, exportFolder); - - var openFolder = AddChildMenu("Open", rootNode); - Add(selectedNode, openFolder); - Add(selectedNode, openFolder); - - var reportsFolder = AddChildMenu("Reports", rootNode); - Add(selectedNode, reportsFolder); - } - - void CreateForDirectory(ContextMenuItem rootNode, TreeNode selectedNode) - { - var isCaPack = selectedNode.FileOwner.IsCaPackFile; - var isCurrentEditable = _packFileService.GetEditablePack() == selectedNode.FileOwner; - - if (selectedNode.NodeType == NodeType.Root) - { - // Close - Add(selectedNode, rootNode); - AddSeperator(rootNode); - - if (!isCaPack && isCurrentEditable == false) - Add(selectedNode, rootNode); - - if (!isCaPack) - { - AddSeperator(rootNode); - Add(selectedNode, rootNode); - Add(selectedNode, rootNode); - AddSeperator(rootNode); - } - } - - if (isCurrentEditable == false) - Add(selectedNode, rootNode); - - if (!isCaPack) - { - var importFolder = AddChildMenu("Import", rootNode); - Add(selectedNode, importFolder); - Add(selectedNode, importFolder); - - if (selectedNode.NodeType != NodeType.Root) - Add(selectedNode, importFolder); - - var createMenu = AddChildMenu("Create", rootNode); - Add(selectedNode, createMenu); - - AddSeperator(rootNode); - Add(selectedNode, rootNode); - Add(selectedNode, rootNode); - AddSeperator(rootNode); - } - - Add(selectedNode, rootNode); - Add(selectedNode, rootNode); - Add(selectedNode, rootNode); - - if (!isCaPack) - Add(selectedNode, rootNode); - } - } -} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/NoContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/NoContextMenuBuilder.cs deleted file mode 100644 index ff09cc3fd..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/NoContextMenuBuilder.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Shared.Core.Events; - -namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu -{ - public class NoContextMenuBuilder : ContextMenuBuilder - { - public NoContextMenuBuilder(IUiCommandFactory commandFactory) : base(ContextMenuType.None, commandFactory) - { - - } - - protected override void Create(ContextMenuItem rootNode, TreeNode selectedNode) { } - } -} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/SimpleContextMenuBuilder.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/SimpleContextMenuBuilder.cs deleted file mode 100644 index ce454a3b0..000000000 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/SimpleContextMenuBuilder.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Shared.Core.Events; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; - -namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu -{ - public class SimpleContextMenuBuilder : ContextMenuBuilder - { - public SimpleContextMenuBuilder(IUiCommandFactory commandFactory) : base(ContextMenuType.Simple, commandFactory) - { - } - - protected override void Create(ContextMenuItem rootNode, TreeNode selectedNode) - { - var nodeType = selectedNode.NodeType; - if (nodeType == NodeType.File) - return; - - Add(selectedNode, rootNode); - Add(selectedNode, rootNode); - AddSeperator(rootNode); - Add(selectedNode, rootNode); - } - } -} diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs index c4671a2e3..02d5354e0 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs @@ -27,7 +27,8 @@ public partial class PackFileBrowserViewModel : ObservableObject, IDisposable, I private readonly IEventHub? _eventHub; private readonly IWindowsKeyboard _windowKeyboard; private readonly ApplicationSettingsService _applicationSettingsService; - private readonly IContextMenuBuilder _contextMenuBuilder; + private readonly PackFileContextMenuComposer _contextMenuComposer; + private readonly ContextMenuType _contextMenuType; private readonly Dictionary _treeRoots = []; public event FileSelectedDelegate FileOpen; @@ -41,13 +42,14 @@ public partial class PackFileBrowserViewModel : ObservableObject, IDisposable, I public bool ShowFoldersOnly { get; } - public PackFileBrowserViewModel(ApplicationSettingsService applicationSettingsService, IContextMenuBuilder contextMenuBuilder, IPackFileService packFileService, IEventHub? eventHub, IWindowsKeyboard windowKeyboard, bool showCaFiles, bool showFoldersOnly) + public PackFileBrowserViewModel(ApplicationSettingsService applicationSettingsService, PackFileContextMenuComposer contextMenuComposer, ContextMenuType contextMenuType, IPackFileService packFileService, IEventHub? eventHub, IWindowsKeyboard windowKeyboard, bool showCaFiles, bool showFoldersOnly) { _packFileService = packFileService; _eventHub = eventHub; _windowKeyboard = windowKeyboard; _applicationSettingsService = applicationSettingsService; - _contextMenuBuilder = contextMenuBuilder; + _contextMenuComposer = contextMenuComposer; + _contextMenuType = contextMenuType; ShowFoldersOnly = showFoldersOnly; @@ -82,7 +84,7 @@ public void Dispose() partial void OnSelectedItemChanged(TreeNode value) { - ContextMenu = _contextMenuBuilder.Build(value); + ContextMenu = _contextMenuComposer.Build(_contextMenuType, value); NodeSelected?.Invoke(_selectedItem); } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileTreeViewFactory.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileTreeViewFactory.cs index afd9689e3..dfa79f7b5 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileTreeViewFactory.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileTreeViewFactory.cs @@ -11,22 +11,21 @@ public class PackFileTreeViewFactory private readonly ApplicationSettingsService _applicationSettingsService; private readonly IPackFileService _packFileService; private readonly IEventHub _eventHub; - private readonly ContextMenuFactory _contextMenuFactory; + private readonly PackFileContextMenuComposer _contextMenuComposer; private readonly IWindowsKeyboard _windowKeyboard; - public PackFileTreeViewFactory(ApplicationSettingsService applicationSettingsService, IPackFileService packFileService, IEventHub eventHub, ContextMenuFactory contextMenuFactory, IWindowsKeyboard windowKeyboard) + public PackFileTreeViewFactory(ApplicationSettingsService applicationSettingsService, IPackFileService packFileService, IEventHub eventHub, PackFileContextMenuComposer contextMenuComposer, IWindowsKeyboard windowKeyboard) { _applicationSettingsService = applicationSettingsService; _packFileService = packFileService; _eventHub = eventHub; - _contextMenuFactory = contextMenuFactory; + _contextMenuComposer = contextMenuComposer; _windowKeyboard = windowKeyboard; } public PackFileBrowserViewModel Create(ContextMenuType contextMenu, bool showCaFiles, bool showFoldersOnly) { - var contextMenuBuilder = _contextMenuFactory.GetContextMenu(contextMenu); - var fileTree = new PackFileBrowserViewModel(_applicationSettingsService, contextMenuBuilder, _packFileService, _eventHub, _windowKeyboard, showCaFiles, showFoldersOnly); + var fileTree = new PackFileBrowserViewModel(_applicationSettingsService, _contextMenuComposer, contextMenu, _packFileService, _eventHub, _windowKeyboard, showCaFiles, showFoldersOnly); return fileTree; } } diff --git a/Shared/SharedUI/Shared.Ui/DependencyInjectionContainer.cs b/Shared/SharedUI/Shared.Ui/DependencyInjectionContainer.cs index f5436f4ed..62f316f4b 100644 --- a/Shared/SharedUI/Shared.Ui/DependencyInjectionContainer.cs +++ b/Shared/SharedUI/Shared.Ui/DependencyInjectionContainer.cs @@ -27,14 +27,19 @@ public override void Register(IServiceCollection services) services.AddScoped(); // Context menu - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddSingleton(provider => + { + var registry = new PackFileContextMenuRegistry(); + RegisterPackFileContextMenuItems(registry); + + foreach (var registration in provider.GetServices()) + registration.Register(registry); + + return registry; + }); + services.AddScoped(); // TODO: Should all be transient? - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -46,16 +51,47 @@ public override void Register(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + } + + private static void RegisterPackFileContextMenuItems(PackFileContextMenuRegistry registry) + { + // MainApplication context menu + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 0, ContextMenuCluster.PackFileOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 10, ContextMenuCluster.PackFileOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 20, ContextMenuCluster.PackFileOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 30, ContextMenuCluster.PackFileOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 40, ContextMenuCluster.PackFileOperation); + + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "Import", priority: 0, ContextMenuCluster.FolderOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "Import", priority: 10, ContextMenuCluster.FolderOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "Create", priority: 30, ContextMenuCluster.FolderOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 40, ContextMenuCluster.FolderOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 50, ContextMenuCluster.FolderOperation); + + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 0, ContextMenuCluster.FileOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 10, ContextMenuCluster.FileOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 20, ContextMenuCluster.FileOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 30, ContextMenuCluster.FileOperation); + + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "Export", priority: 0, ContextMenuCluster.Export); + + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 0, ContextMenuCluster.Misc); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 10, ContextMenuCluster.Misc); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "", priority: 20, ContextMenuCluster.Misc); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "Open", priority: 30, ContextMenuCluster.Misc); + registry.RegisterPackFileContextMenuItem(ContextMenuType.MainApplication, path: "Open", priority: 40, ContextMenuCluster.Misc); - // Interfaces for context menu commands implemented externally - // IRmvToTextCommand is registered in Editors.Reports + // Simple context menu + registry.RegisterPackFileContextMenuItem(ContextMenuType.Simple, path: "", priority: 0, ContextMenuCluster.FolderOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.Simple, path: "", priority: 10, ContextMenuCluster.FolderOperation); + registry.RegisterPackFileContextMenuItem(ContextMenuType.Simple, path: "", priority: 20, ContextMenuCluster.FolderOperation); } public override void RegisterTools(IEditorDatabase factory) From 7e41143a3ab7a29120ba719ac0492ad00ddec40b Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 13:46:14 +0200 Subject: [PATCH 17/26] Code --- .../Commands/ClosePackContainerFileCommand.cs | 8 ++-- .../Commands/DuplicateFileCommand.cs | 11 ++++- .../Commands/ExportToDirectoryCommand.cs | 28 ++++++------ .../Commands/ImportDirectoryCommand.cs | 7 +-- .../Commands/ImportFilesCommand.cs | 7 +-- .../ContextMenu/Commands/OpenNodeInCommand.cs | 29 ++++++++----- .../Commands/OpenPackInFileExplorerCommand.cs | 15 ++++++- .../SaveAsPackFileContainerCommand.cs | 27 +++++++++--- .../Commands/SavePackFileContainerCommand.cs | 8 ++-- Shared/SharedUI/Shared.Ui/Shared.Ui.csproj | 6 +++ .../ClosePackContainerFileCommandTests.cs | 35 +++++++++++++++ .../CollapseNodeCommandTests.cs | 39 +++++++++++++++++ .../ContextMenuCommandTestBase.cs | 18 ++++++++ .../CopyNodePathCommandTests.cs | 33 ++++++++++++++ .../CopyToEditablePackCommandTests.cs | 43 +++++++++++++++++++ .../CreateFolderCommandTests.cs | 36 ++++++++++++++++ .../DeleteNodeCommandTests.cs | 37 ++++++++++++++++ .../DuplicateFileCommandTests.cs | 38 ++++++++++++++++ .../ExpandNodeCommandTests.cs | 41 ++++++++++++++++++ .../ExportToDirectoryCommandTests.cs | 42 ++++++++++++++++++ .../ImportDirectoryCommandTests.cs | 35 +++++++++++++++ .../ImportFileCommandTests.cs | 35 +++++++++++++++ .../OpenNodeInHxDCommandTests.cs | 29 +++++++++++++ .../OpenNodeInNotepadCommandTests.cs | 29 +++++++++++++ .../OpenPackInFileExplorerCommandTests.cs | 41 ++++++++++++++++++ .../RenameNodeCommandTests.cs | 36 ++++++++++++++++ .../SaveAsPackFileContainerCommandTests.cs | 29 +++++++++++++ .../SavePackFileContainerCommandTests.cs | 34 +++++++++++++++ .../SetAsEditablePackCommandTests.cs | 31 +++++++++++++ 29 files changed, 760 insertions(+), 47 deletions(-) create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ClosePackContainerFileCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CollapseNodeCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ContextMenuCommandTestBase.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyNodePathCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyToEditablePackCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CreateFolderCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DeleteNodeCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DuplicateFileCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExpandNodeCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExportToDirectoryCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportDirectoryCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportFileCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInHxDCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInNotepadCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenPackInFileExplorerCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/RenameNodeCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SaveAsPackFileContainerCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SavePackFileContainerCommandTests.cs create mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SetAsEditablePackCommandTests.cs diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommand.cs index f6dfd46ed..251e88006 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommand.cs @@ -1,9 +1,9 @@ -using System.Windows; -using Shared.Core.PackFiles; +using Shared.Core.PackFiles; +using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class ClosePackContainerFileCommand(IPackFileService packFileService) : IContextMenuCommand + public class ClosePackContainerFileCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Close"; public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Root; @@ -11,7 +11,7 @@ public class ClosePackContainerFileCommand(IPackFileService packFileService) : I public void Execute(TreeNode selectedNode) { - if (MessageBox.Show("Are you sure you want to close the packfile?", "", MessageBoxButton.YesNo) == MessageBoxResult.Yes) + if (standardDialogs.ShowYesNoBox("Are you sure you want to close the packfile?", "") == ShowMessageBoxResult.OK) packFileService.UnloadPackContainer(selectedNode.FileOwner); } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommand.cs index 5c6dbebfc..5e7f08696 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommand.cs @@ -2,10 +2,11 @@ using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class DuplicateFileCommand(IPackFileService packFileService) : IContextMenuCommand + public class DuplicateFileCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Duplicate"; public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.File && node.Item != null && !node.FileOwner.IsCaPackFile; @@ -29,11 +30,17 @@ public void Execute(PackFile item) private void ReadAndSave(string newName, PackFile item) { + var editablePack = packFileService.GetEditablePack(); + if (editablePack == null) + { + standardDialogs.ShowDialogBox("No editable pack selected."); + return; + } + var bytes = item.DataSource.ReadData(); var packFile = new PackFile(newName, new MemorySource(bytes)); var parentPath = packFileService.GetFullPath(item); var path = Path.GetDirectoryName(parentPath); - var editablePack = packFileService.GetEditablePack(); var fileEntry = new NewPackFileEntry(path, packFile); packFileService.AddFilesToPack(editablePack, [fileEntry]); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs index 013d2aebc..c3f6aa413 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs @@ -1,10 +1,12 @@ -using System.IO; +using System; +using System.IO; using System.Windows.Forms; using Shared.Core.Misc; +using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class ExportToDirectoryCommand() : IContextMenuCommand + public class ExportToDirectoryCommand(IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Export to system folder"; public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Directory || node.NodeType == NodeType.Root || (node.NodeType == NodeType.File && node.Item != null); @@ -13,13 +15,13 @@ public class ExportToDirectoryCommand() : IContextMenuCommand public void Execute(TreeNode selectedNode) { // TODO: Fix bug where if you export the packfilecontainer itself it doesn't export correctly. - var dialog = new FolderBrowserDialog(); + using var dialog = new FolderBrowserDialog(); if (dialog.ShowDialog() == DialogResult.OK) { var nodeStartDir = Path.GetDirectoryName(selectedNode.GetFullPath()); var fileCounter = 0; SaveSelfAndChildren(selectedNode, dialog.SelectedPath, nodeStartDir, ref fileCounter); - MessageBox.Show($"{fileCounter} files exported!"); + standardDialogs.ShowDialogBox($"{fileCounter} files exported!", "Export"); } } @@ -33,14 +35,7 @@ void SaveSelfAndChildren(TreeNode node, string outputDirectory, string rootPath, else { var nodeOriginalPath = node.GetFullPath(); - - var nodePathWithoutRoot = nodeOriginalPath; - if (rootPath != null && rootPath.Length != 0) - nodePathWithoutRoot = nodeOriginalPath.Replace(rootPath, ""); - - if (nodePathWithoutRoot.StartsWith("\\") == false) - nodePathWithoutRoot = "\\" + nodePathWithoutRoot; - + var nodePathWithoutRoot = ComputeRelativePath(nodeOriginalPath, rootPath); var fileOutputPath = outputDirectory + nodePathWithoutRoot; var fileOutputDir = Path.GetDirectoryName(fileOutputPath); @@ -55,6 +50,15 @@ void SaveSelfAndChildren(TreeNode node, string outputDirectory, string rootPath, } } + internal static string ComputeRelativePath(string nodeFullPath, string? rootPath) + { + var relative = nodeFullPath; + if (!string.IsNullOrEmpty(rootPath) && nodeFullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase)) + relative = nodeFullPath[rootPath.Length..]; + if (!relative.StartsWith("\\")) + relative = "\\" + relative; + return relative; + } } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs index 2718518d0..2e50ffb53 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs @@ -5,10 +5,11 @@ using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class ImportDirectoryCommand(IPackFileService packFileService) : IContextMenuCommand + public class ImportDirectoryCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Import Directory"; public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; @@ -18,11 +19,11 @@ public void Execute(TreeNode _selectedNode) { if (_selectedNode.FileOwner.IsCaPackFile) { - System.Windows.MessageBox.Show("Unable to edit CA packfile"); + standardDialogs.ShowDialogBox("Unable to edit CA packfile"); return; } - var dialog = new FolderBrowserDialog(); + using var dialog = new FolderBrowserDialog(); if (dialog.ShowDialog() == DialogResult.OK) { var folderPath = dialog.SelectedPath; diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs index 2d103eeb3..6534b43bf 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs @@ -3,10 +3,11 @@ using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.FileSources; +using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class ImportFileCommand(IPackFileService packFileService) : IContextMenuCommand + public class ImportFileCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Import File"; public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; @@ -16,11 +17,11 @@ public void Execute(TreeNode _selectedNode) { if (_selectedNode.FileOwner.IsCaPackFile) { - System.Windows.MessageBox.Show("Unable to edit CA packfile"); + standardDialogs.ShowDialogBox("Unable to edit CA packfile"); return; } - var dialog = new OpenFileDialog() + using var dialog = new OpenFileDialog() { Multiselect = true, }; diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs index 389a8c99d..84027bf55 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs @@ -1,12 +1,12 @@ -using System.Diagnostics; -using System; -using System.Windows; -using Shared.Core.PackFiles.Models; +using System; +using System.Diagnostics; using System.IO; +using Shared.Core.PackFiles.Models; +using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public abstract class OpenNodeInCommand() : IContextMenuCommand + public abstract class OpenNodeInCommand(IStandardDialogs standardDialogs) : IContextMenuCommand { public abstract string GetDisplayName(TreeNode node); public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.File && node.Item != null; @@ -18,7 +18,7 @@ protected void OpenPackFileUsing(string applicationPath, PackFile packFile) { if (File.Exists(applicationPath) == false) { - MessageBox.Show($"Application {applicationPath} does not exist"); + standardDialogs.ShowDialogBox($"Application {applicationPath} does not exist"); return; } @@ -29,19 +29,26 @@ protected void OpenPackFileUsing(string applicationPath, PackFile packFile) var bytes = packFile.DataSource.ReadData(); File.WriteAllBytes(path, bytes); - Process.Start(applicationPath, $"\"{path}\""); + using var process = Process.Start(applicationPath, $"\"{path}\""); + } + + protected static string ResolveApplicationPath(string appRelativePath) + { + var x64 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), appRelativePath); + if (File.Exists(x64)) return x64; + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), appRelativePath); } } - public class OpenNodeInNotepadCommand() : OpenNodeInCommand + public class OpenNodeInNotepadCommand(IStandardDialogs standardDialogs) : OpenNodeInCommand(standardDialogs) { public override string GetDisplayName(TreeNode node) => "Open in Notepad++"; - public override void Execute(TreeNode _selectedNode) => OpenPackFileUsing(@"C:\Program Files\Notepad++\notepad++.exe", _selectedNode.Item!); + public override void Execute(TreeNode _selectedNode) => OpenPackFileUsing(ResolveApplicationPath(@"Notepad++\notepad++.exe"), _selectedNode.Item!); } - public class OpenNodeInHxDCommand() : OpenNodeInCommand + public class OpenNodeInHxDCommand(IStandardDialogs standardDialogs) : OpenNodeInCommand(standardDialogs) { public override string GetDisplayName(TreeNode node) => "Open in Hxd"; - public override void Execute(TreeNode _selectedNode) => OpenPackFileUsing(@"C:\Program Files\HxD\HxD.exe", _selectedNode.Item!); + public override void Execute(TreeNode _selectedNode) => OpenPackFileUsing(ResolveApplicationPath(@"HxD\HxD.exe"), _selectedNode.Item!); } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs index 5e25f5fef..4e976550f 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs @@ -1,10 +1,11 @@ using System.Diagnostics; using System.IO; using Shared.Core.PackFiles; +using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class OpenPackInFileExplorerCommand(IPackFileService packFileService) : IContextMenuCommand + public class OpenPackInFileExplorerCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Open In File Explorer"; public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; @@ -13,9 +14,21 @@ public class OpenPackInFileExplorerCommand(IPackFileService packFileService) : I public void Execute(TreeNode _selectedNode) { var systemFilePath = _selectedNode.FileOwner.SystemFilePath; + if (string.IsNullOrEmpty(systemFilePath)) + { + standardDialogs.ShowDialogBox("Pack file has not been saved to disk yet."); + return; + } + if (!Directory.Exists(systemFilePath)) systemFilePath = Path.GetDirectoryName(systemFilePath); + if (systemFilePath == null) + { + standardDialogs.ShowDialogBox("Unable to determine folder for pack file."); + return; + } + Process.Start(new ProcessStartInfo { FileName = "explorer.exe", diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs index 4cf02d096..b1d713bc0 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs @@ -1,19 +1,24 @@ -using System.Windows.Forms; +using System; +using System.Windows.Forms; +using Serilog; +using Shared.Core.ErrorHandling; using Shared.Core.PackFiles; +using Shared.Core.Services; using Shared.Core.Settings; using Shared.Ui.Common; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class SaveAsPackFileContainerCommand(IPackFileService packFileService, ApplicationSettingsService applicationSettingsService) : IContextMenuCommand + public class SaveAsPackFileContainerCommand(IPackFileService packFileService, ApplicationSettingsService applicationSettingsService, IStandardDialogs standardDialogs) : IContextMenuCommand { + private readonly ILogger _logger = Logging.Create(); public string GetDisplayName(TreeNode node) => "Save As"; public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Root && !node.FileOwner.IsCaPackFile; public bool IsEnabled(TreeNode node) => true; public void Execute(TreeNode _selectedNode) { - var saveFileDialog = new SaveFileDialog(); + using var saveFileDialog = new SaveFileDialog(); saveFileDialog.FileName = _selectedNode.FileOwner.Name; saveFileDialog.Filter = "PackFile | *.pack"; saveFileDialog.DefaultExt = "pack"; @@ -22,10 +27,18 @@ public void Execute(TreeNode _selectedNode) using (new WaitCursor()) { - var gameInformation = GameInformationDatabase.GetGameById(applicationSettingsService.CurrentSettings.CurrentGame); - packFileService.SavePackContainer(_selectedNode.FileOwner, saveFileDialog.FileName, false, gameInformation); - _selectedNode.UnsavedChanged = false; - _selectedNode.ForeachNode((node) => node.UnsavedChanged = false); + try + { + var gameInformation = GameInformationDatabase.GetGameById(applicationSettingsService.CurrentSettings.CurrentGame); + packFileService.SavePackContainer(_selectedNode.FileOwner, saveFileDialog.FileName, false, gameInformation); + _selectedNode.UnsavedChanged = false; + _selectedNode.ForeachNode((node) => node.UnsavedChanged = false); + } + catch (Exception e) + { + _logger.Here().Error(e, "Exception while saving"); + standardDialogs.ShowDialogBox("Error saving:\n\n" + e.Message, "Error"); + } } } } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs index 2d465de83..efab1636d 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs @@ -24,7 +24,7 @@ public void Execute(TreeNode _selectedNode) var systemPath = _selectedNode.FileOwner.SystemFilePath; if (string.IsNullOrWhiteSpace(systemPath)) { - var saveFileDialog = new SaveFileDialog(); + using var saveFileDialog = new SaveFileDialog(); saveFileDialog.FileName = _selectedNode.FileOwner.Name; saveFileDialog.Filter = "PackFile | *.pack"; saveFileDialog.DefaultExt = "pack"; @@ -43,7 +43,7 @@ public void Execute(TreeNode _selectedNode) catch (Exception e) { _logger.Here().Error(e, "Exception while saving"); - System.Windows.MessageBox.Show("Error saving:\n\n" + e.Message, "Error"); + standardDialogs.ShowDialogBox("Error saving:\n\n" + e.Message, "Error"); } } } @@ -60,7 +60,7 @@ public void Execute() var systemPath = pack.SystemFilePath; if (string.IsNullOrWhiteSpace(systemPath)) { - var saveFileDialog = new SaveFileDialog(); + using var saveFileDialog = new SaveFileDialog(); saveFileDialog.FileName = pack.Name; saveFileDialog.Filter = "PackFile | *.pack"; saveFileDialog.DefaultExt = "pack"; @@ -79,7 +79,7 @@ public void Execute() catch (Exception e) { _logger.Here().Error(e, "Exception while saving"); - System.Windows.MessageBox.Show("Error saving:\n\n" + e.Message, "Error"); + standardDialogs.ShowDialogBox("Error saving:\n\n" + e.Message, "Error"); } } } diff --git a/Shared/SharedUI/Shared.Ui/Shared.Ui.csproj b/Shared/SharedUI/Shared.Ui/Shared.Ui.csproj index 26c6703e6..cb9c550e7 100644 --- a/Shared/SharedUI/Shared.Ui/Shared.Ui.csproj +++ b/Shared/SharedUI/Shared.Ui/Shared.Ui.csproj @@ -19,6 +19,12 @@ + + + <_Parameter1>Shared.UiTest + + + Code diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ClosePackContainerFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ClosePackContainerFileCommandTests.cs new file mode 100644 index 000000000..da469c61b --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ClosePackContainerFileCommandTests.cs @@ -0,0 +1,35 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class ClosePackContainerFileCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var service = new Mock(); + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowYesNoBox(It.IsAny(), It.IsAny())).Returns(ShowMessageBoxResult.OK); + + var command = new ClosePackContainerFileCommand(service.Object, dialogs.Object); + + Assert.That(command.ShouldAdd(root), Is.True); + Assert.That(command.ShouldAdd(new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a"))), Is.False); + Assert.That(command.IsEnabled(root), Is.True); + + command.Execute(root); + + service.Verify(x => x.UnloadPackContainer(owner), Times.Once); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CollapseNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CollapseNodeCommandTests.cs new file mode 100644 index 000000000..198c36e27 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CollapseNodeCommandTests.cs @@ -0,0 +1,39 @@ +using System.Threading; +using Shared.Core.PackFiles.Models; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class CollapseNodeCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var folder = new TreeNode("folder", NodeType.Directory, owner, root); + var file = new TreeNode("file.txt", NodeType.File, owner, folder, PackFile.CreateFromASCII("file.txt", "a")); + root.AddChild(folder); + folder.AddChild(file); + root.Children.Add(folder); + folder.Children.Add(file); + + root.IsNodeExpanded = true; + folder.IsNodeExpanded = true; + file.IsNodeExpanded = true; + + var command = new CollapseNodeCommand(); + + Assert.That(command.ShouldAdd(folder), Is.True); + Assert.That(command.ShouldAdd(file), Is.False); + Assert.That(command.IsEnabled(folder), Is.True); + + command.Execute(root); + + Assert.That(root.IsNodeExpanded, Is.False); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ContextMenuCommandTestBase.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ContextMenuCommandTestBase.cs new file mode 100644 index 000000000..a55dfa56f --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ContextMenuCommandTestBase.cs @@ -0,0 +1,18 @@ +using Moq; +using Shared.Core.PackFiles.Models; +using Shared.Ui.BaseDialogs.PackFileTree; + +namespace Shared.UiTest.ContextMenuCommands +{ + internal abstract class ContextMenuCommandTestBase + { + protected static IPackFileContainer CreateContainer(bool isCa = false, string name = "pack", string systemFilePath = "C:\\temp\\pack.pack") + { + var container = new Mock(); + container.SetupGet(x => x.Name).Returns(name); + container.SetupGet(x => x.SystemFilePath).Returns(systemFilePath); + container.SetupProperty(x => x.IsCaPackFile, isCa); + return container.Object; + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyNodePathCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyNodePathCommandTests.cs new file mode 100644 index 000000000..6f7f02bf6 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyNodePathCommandTests.cs @@ -0,0 +1,33 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class CopyNodePathCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var service = new Mock(); + service.Setup(x => x.GetFullPath(file.Item!, null)).Returns("folder\\file.txt"); + + var command = new CopyNodePathCommand(service.Object); + + Assert.That(command.ShouldAdd(file), Is.True); + Assert.That(command.IsEnabled(file), Is.True); + + command.Execute(file); + + Assert.That(System.Windows.Clipboard.GetText(), Is.EqualTo("folder\\file.txt")); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyToEditablePackCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyToEditablePackCommandTests.cs new file mode 100644 index 000000000..4c8672f3c --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyToEditablePackCommandTests.cs @@ -0,0 +1,43 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class CopyToEditablePackCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var source = CreateContainer(name: "source"); + var target = CreateContainer(name: "target"); + var service = new Mock(); + service.Setup(x => x.GetEditablePack()).Returns(target); + + var dialogs = new Mock(); + var waitCursor = new Mock(); + dialogs.Setup(x => x.ShowWaitCursor()).Returns(waitCursor.Object); + + var root = new TreeNode("root", NodeType.Root, source, null); + var folder = new TreeNode("folder", NodeType.Directory, source, root); + root.AddChild(folder); + var file = new TreeNode("file.txt", NodeType.File, source, folder, PackFile.CreateFromASCII("file.txt", "a")); + folder.AddChild(file); + + var command = new CopyToEditablePackCommand(service.Object, dialogs.Object); + + Assert.That(command.ShouldAdd(folder), Is.True); + Assert.That(command.IsEnabled(folder), Is.True); + + command.Execute(folder); + + service.Verify(x => x.CopyFileFromOtherPackFile(source, It.IsAny(), target), Times.Once); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CreateFolderCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CreateFolderCommandTests.cs new file mode 100644 index 000000000..b0c390d57 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CreateFolderCommandTests.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Moq; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class CreateFolderCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var existing = new TreeNode("existing", NodeType.Directory, owner, root); + root.AddChild(existing); + + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowFolderNameDialog(It.IsAny>(), It.IsAny())).Returns("new_folder"); + + var command = new CreateFolderCommand(dialogs.Object); + + Assert.That(command.ShouldAdd(root), Is.True); + Assert.That(command.IsEnabled(root), Is.True); + + command.Execute(root); + + Assert.That(root.BackingChildren.Any(x => x.Name == "new_folder"), Is.True); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DeleteNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DeleteNodeCommandTests.cs new file mode 100644 index 000000000..d2248bddd --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DeleteNodeCommandTests.cs @@ -0,0 +1,37 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class DeleteNodeCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + root.AddChild(file); + + var service = new Mock(); + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowYesNoBox(It.IsAny(), It.IsAny())).Returns(ShowMessageBoxResult.OK); + + var command = new DeleteNodeCommand(service.Object, dialogs.Object); + + Assert.That(command.ShouldAdd(file), Is.True); + Assert.That(command.IsEnabled(file), Is.True); + + command.Execute(file); + + service.Verify(x => x.DeleteFile(owner, file.Item!), Times.Once); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DuplicateFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DuplicateFileCommandTests.cs new file mode 100644 index 000000000..7f5a3b0eb --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DuplicateFileCommandTests.cs @@ -0,0 +1,38 @@ +using System.Threading; +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; +using Test.TestingUtility.Shared; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class DuplicateFileCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var runner = new AssetEditorTestRunner(); + runner.PackFileService.EnforceGameFilesMustBeLoaded = false; + var sourcePackFile = runner.CreateEmptyPackFile("SourcePack", false); + var outputPackFile = runner.CreateEmptyPackFile("OutputPack", true); + + var fileEntry = new NewPackFileEntry("Animation\\Meta", PackFile.CreateFromASCII("testFile.anm", "DummyContent")); + runner.PackFileService.AddFilesToPack(sourcePackFile, [fileEntry]); + var fileToCopy = runner.PackFileService.FindFile("Animation\\Meta\\testFile.anm", sourcePackFile)!; + + var node = new TreeNode(fileToCopy.Name, NodeType.File, sourcePackFile, null, fileToCopy); + var command = runner.CommandFactory.Create(); + + Assert.That(command.ShouldAdd(node), Is.True); + Assert.That(command.IsEnabled(node), Is.True); + + command.Execute(node); + + var foundFile = runner.PackFileService.FindFile("Animation\\Meta\\testFile_copy.anm", outputPackFile); + Assert.That(foundFile, Is.Not.Null); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExpandNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExpandNodeCommandTests.cs new file mode 100644 index 000000000..5a19e6cd7 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExpandNodeCommandTests.cs @@ -0,0 +1,41 @@ +using System.Threading; +using Shared.Core.PackFiles.Models; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class ExpandNodeCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var folder = new TreeNode("folder", NodeType.Directory, owner, root); + var file = new TreeNode("file.txt", NodeType.File, owner, folder, PackFile.CreateFromASCII("file.txt", "a")); + root.AddChild(folder); + folder.AddChild(file); + root.Children.Add(folder); + folder.Children.Add(file); + + root.IsNodeExpanded = false; + folder.IsNodeExpanded = false; + file.IsNodeExpanded = false; + + var command = new ExpandNodeCommand(); + + Assert.That(command.ShouldAdd(folder), Is.True); + Assert.That(command.ShouldAdd(file), Is.False); + Assert.That(command.IsEnabled(folder), Is.True); + + command.Execute(root); + + Assert.That(root.IsNodeExpanded, Is.True); + Assert.That(folder.IsNodeExpanded, Is.True); + Assert.That(file.IsNodeExpanded, Is.True); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExportToDirectoryCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExportToDirectoryCommandTests.cs new file mode 100644 index 000000000..be057c5c8 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExportToDirectoryCommandTests.cs @@ -0,0 +1,42 @@ +using System.Threading; +using Moq; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class ExportToDirectoryCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new ExportToDirectoryCommand(new Mock().Object); + + Assert.That(command.ShouldAdd(root), Is.True); + Assert.That(command.IsEnabled(root), Is.True); + + // TODO: Execute uses FolderBrowserDialog and writes exported files to disk; skip for filesystem pass two. + Assert.Ignore("TODO: Execute uses FolderBrowserDialog and writes exported files to disk; skip for filesystem pass two."); + } + + [Test] + public void ComputeRelativePath_RepeatedSegmentDoesNotCorruptPath() + { + // Root dir is "data", file fullPath is "data\data\unit.mesh" — old Replace would strip both occurrences. + var result = ExportToDirectoryCommand.ComputeRelativePath("data\\data\\unit.mesh", "data"); + Assert.That(result, Is.EqualTo("\\data\\unit.mesh")); + } + + [Test] + public void ComputeRelativePath_NullRootReturnsFullPath() + { + var result = ExportToDirectoryCommand.ComputeRelativePath("sub\\file.mesh", null); + Assert.That(result, Is.EqualTo("\\sub\\file.mesh")); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportDirectoryCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportDirectoryCommandTests.cs new file mode 100644 index 000000000..139e386fe --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportDirectoryCommandTests.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class ImportDirectoryCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute_CaPackShowsError() + { + var owner = CreateContainer(isCa: true); + var root = new TreeNode("root", NodeType.Root, owner, null); + + var service = new Mock(); + var dialogs = new Mock(); + var command = new ImportDirectoryCommand(service.Object, dialogs.Object); + + Assert.That(command.ShouldAdd(new TreeNode("dir", NodeType.Directory, CreateContainer(), null)), Is.True); + Assert.That(command.IsEnabled(root), Is.True); + + command.Execute(root); + + dialogs.Verify(x => x.ShowDialogBox("Unable to edit CA packfile", "Error"), Times.Once); + service.Verify(x => x.AddFilesToPack(It.IsAny(), It.IsAny>()), Times.Never); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportFileCommandTests.cs new file mode 100644 index 000000000..1e8c18960 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportFileCommandTests.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class ImportFileCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute_CaPackShowsError() + { + var owner = CreateContainer(isCa: true); + var root = new TreeNode("root", NodeType.Root, owner, null); + + var service = new Mock(); + var dialogs = new Mock(); + var command = new ImportFileCommand(service.Object, dialogs.Object); + + Assert.That(command.ShouldAdd(new TreeNode("dir", NodeType.Directory, CreateContainer(), null)), Is.True); + Assert.That(command.IsEnabled(root), Is.True); + + command.Execute(root); + + dialogs.Verify(x => x.ShowDialogBox("Unable to edit CA packfile", "Error"), Times.Once); + service.Verify(x => x.AddFilesToPack(It.IsAny(), It.IsAny>()), Times.Never); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInHxDCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInHxDCommandTests.cs new file mode 100644 index 000000000..0fb5a0afe --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInHxDCommandTests.cs @@ -0,0 +1,29 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles.Models; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class OpenNodeInHxDCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new OpenNodeInHxDCommand(new Mock().Object); + + Assert.That(command.ShouldAdd(file), Is.True); + Assert.That(command.IsEnabled(file), Is.True); + + // TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass. + Assert.Ignore("TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass."); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInNotepadCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInNotepadCommandTests.cs new file mode 100644 index 000000000..c282677f4 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInNotepadCommandTests.cs @@ -0,0 +1,29 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles.Models; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class OpenNodeInNotepadCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new OpenNodeInNotepadCommand(new Mock().Object); + + Assert.That(command.ShouldAdd(file), Is.True); + Assert.That(command.IsEnabled(file), Is.True); + + // TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass. + Assert.Ignore("TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass."); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenPackInFileExplorerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenPackInFileExplorerCommandTests.cs new file mode 100644 index 000000000..8fe449a5d --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenPackInFileExplorerCommandTests.cs @@ -0,0 +1,41 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class OpenPackInFileExplorerCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new OpenPackInFileExplorerCommand(new Mock().Object, new Mock().Object); + + Assert.That(command.ShouldAdd(root), Is.True); + Assert.That(command.IsEnabled(root), Is.True); + + // TODO: Execute launches explorer.exe (external process); skip in first pass. + Assert.Ignore("TODO: Execute launches explorer.exe (external process); skip in first pass."); + } + + [Test] + public void Execute_NullSystemFilePath_ShowsError() + { + var owner = CreateContainer(systemFilePath: ""); + var root = new TreeNode("root", NodeType.Root, owner, null); + var dialogs = new Mock(); + var command = new OpenPackInFileExplorerCommand(new Mock().Object, dialogs.Object); + + command.Execute(root); + + dialogs.Verify(x => x.ShowDialogBox(It.IsAny(), It.IsAny()), Times.Once); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/RenameNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/RenameNodeCommandTests.cs new file mode 100644 index 000000000..a80d2f908 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/RenameNodeCommandTests.cs @@ -0,0 +1,36 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.Services; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class RenameNodeCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + root.AddChild(file); + + var service = new Mock(); + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowTextInputDialog("Rename file", file.Name)).Returns(new TextInputDialogResult(true, "renamed.txt")); + var command = new RenameNodeCommand(service.Object, dialogs.Object); + + Assert.That(command.ShouldAdd(file), Is.True); + Assert.That(command.IsEnabled(file), Is.True); + + command.Execute(file); + + service.Verify(x => x.RenameFile(owner, file.Item!, "renamed.txt"), Times.Once); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SaveAsPackFileContainerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SaveAsPackFileContainerCommandTests.cs new file mode 100644 index 000000000..7b77a8ba2 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SaveAsPackFileContainerCommandTests.cs @@ -0,0 +1,29 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.Services; +using Shared.Core.Settings; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class SaveAsPackFileContainerCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new SaveAsPackFileContainerCommand(new Mock().Object, new ApplicationSettingsService(GameTypeEnum.Warhammer3), new Mock().Object); + + Assert.That(command.ShouldAdd(root), Is.True); + Assert.That(command.IsEnabled(root), Is.True); + + // TODO: Execute uses SaveFileDialog and writes pack file to disk; skip for filesystem pass two. + Assert.Ignore("TODO: Execute uses SaveFileDialog and writes pack file to disk; skip for filesystem pass two."); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SavePackFileContainerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SavePackFileContainerCommandTests.cs new file mode 100644 index 000000000..c67545474 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SavePackFileContainerCommandTests.cs @@ -0,0 +1,34 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.Services; +using Shared.Core.Settings; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class SavePackFileContainerCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var owner = CreateContainer(systemFilePath: "C:\\temp\\pack.pack"); + var root = new TreeNode("root", NodeType.Root, owner, null); + var service = new Mock(); + var dialogs = new Mock(); + var appSettings = new ApplicationSettingsService(GameTypeEnum.Warhammer3); + + var command = new SavePackFileContainerCommand(service.Object, dialogs.Object, appSettings); + + Assert.That(command.ShouldAdd(root), Is.True); + Assert.That(command.IsEnabled(root), Is.True); + + command.Execute(root); + + service.Verify(x => x.SavePackContainer(owner, owner.SystemFilePath, false, It.IsAny()), Times.Once); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SetAsEditablePackCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SetAsEditablePackCommandTests.cs new file mode 100644 index 000000000..183eeabf8 --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SetAsEditablePackCommandTests.cs @@ -0,0 +1,31 @@ +using System.Threading; +using Moq; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; + +namespace Shared.UiTest.ContextMenuCommands +{ + [TestFixture] + [Apartment(ApartmentState.STA)] + internal class SetAsEditablePackCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_IsEnabled_Execute() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var service = new Mock(); + service.Setup(x => x.GetEditablePack()).Returns((IPackFileContainer?)null); + var command = new SetAsEditablePackCommand(service.Object); + + Assert.That(command.ShouldAdd(root), Is.True); + Assert.That(command.IsEnabled(root), Is.True); + + command.Execute(root); + + service.Verify(x => x.SetEditablePack(owner), Times.Once); + } + } +} From 753ca1d82bc002de629083c566e2fb2a6b6ec54b Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 14:16:40 +0200 Subject: [PATCH 18/26] Unit tests --- .../Shared.Core/Services/FileSystemAccess.cs | 21 ++++++ .../Shared.Core/Services/IFileSystemAccess.cs | 20 ++++++ .../Shared.Core/Services/IStandardDialogs.cs | 6 ++ .../Commands/ExportToDirectoryCommand.cs | 15 ++--- .../Commands/ImportDirectoryCommand.cs | 15 ++--- .../Commands/ImportFilesCommand.cs | 11 +--- .../ContextMenu/Commands/OpenNodeInCommand.cs | 16 ++--- .../Commands/OpenPackInFileExplorerCommand.cs | 9 ++- .../SaveAsPackFileContainerCommand.cs | 10 +-- .../Commands/SavePackFileContainerCommand.cs | 23 +++---- .../StandardDialog/StandardDialogs.cs | 49 ++++++++++++-- .../Shared.Ui/DependencyInjectionContainer.cs | 1 + .../DuplicateFileCommandTests.cs | 38 ----------- .../ClosePackContainerFileCommandTests.cs | 34 +++++++--- .../Commands}/CollapseNodeCommandTests.cs | 36 ++++++++--- .../Commands}/ContextMenuCommandTestBase.cs | 5 +- .../Commands}/CopyNodePathCommandTests.cs | 33 ++++++++-- .../CopyToEditablePackCommandTests.cs | 33 ++++++++-- .../Commands}/CreateFolderCommandTests.cs | 30 +++++++-- .../Commands}/DeleteNodeCommandTests.cs | 32 ++++++++-- .../Commands/DuplicateFileCommandTests.cs | 64 +++++++++++++++++++ .../Commands}/ExpandNodeCommandTests.cs | 35 +++++++--- .../ExportToDirectoryCommandTests.cs | 26 ++++++-- .../Commands}/ImportDirectoryCommandTests.cs | 29 ++++++--- .../Commands}/ImportFileCommandTests.cs | 27 ++++++-- .../Commands}/OpenNodeInHxDCommandTests.cs | 23 +++++-- .../OpenNodeInNotepadCommandTests.cs | 23 +++++-- .../OpenPackInFileExplorerCommandTests.cs | 24 +++++-- .../Commands}/RenameNodeCommandTests.cs | 32 ++++++++-- .../SaveAsPackFileContainerCommandTests.cs | 20 ++++-- .../SavePackFileContainerCommandTests.cs | 30 +++++++-- .../SetAsEditablePackCommandTests.cs | 30 +++++++-- .../Shared.UiTest/Shared.UiTest.csproj | 4 ++ 33 files changed, 584 insertions(+), 220 deletions(-) create mode 100644 Shared/SharedCore/Shared.Core/Services/FileSystemAccess.cs create mode 100644 Shared/SharedCore/Shared.Core/Services/IFileSystemAccess.cs delete mode 100644 Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DuplicateFileCommandTests.cs rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/ClosePackContainerFileCommandTests.cs (51%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/CollapseNodeCommandTests.cs (55%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/ContextMenuCommandTestBase.cs (85%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/CopyNodePathCommandTests.cs (50%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/CopyToEditablePackCommandTests.cs (54%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/CreateFolderCommandTests.cs (59%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/DeleteNodeCommandTests.cs (53%) create mode 100644 Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/ExpandNodeCommandTests.cs (58%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/ExportToDirectoryCommandTests.cs (63%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/ImportDirectoryCommandTests.cs (57%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/ImportFileCommandTests.cs (61%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/OpenNodeInHxDCommandTests.cs (57%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/OpenNodeInNotepadCommandTests.cs (57%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/OpenPackInFileExplorerCommandTests.cs (65%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/RenameNodeCommandTests.cs (54%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/SaveAsPackFileContainerCommandTests.cs (62%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/SavePackFileContainerCommandTests.cs (52%) rename Shared/SharedUI/Shared.UiTest/{ContextMenuCommands => PackFileTree/ContextMenu/Commands}/SetAsEditablePackCommandTests.cs (55%) diff --git a/Shared/SharedCore/Shared.Core/Services/FileSystemAccess.cs b/Shared/SharedCore/Shared.Core/Services/FileSystemAccess.cs new file mode 100644 index 000000000..3e3053bff --- /dev/null +++ b/Shared/SharedCore/Shared.Core/Services/FileSystemAccess.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; +using System.IO; + +namespace Shared.Core.Services +{ + public class FileSystemAccess : IFileSystemAccess + { + public string? PathGetDirectoryName(string path) => Path.GetDirectoryName(path); + public string PathGetFileName(string path) => Path.GetFileName(path); + + public void FileWriteAllBytes(string path, byte[] bytes) => File.WriteAllBytes(path, bytes); + public bool FileExists(string path) => File.Exists(path); + + public bool DirectoryExists(string path) => Directory.Exists(path); + public string[] DirectoryGetFiles(string path, string searchPattern, SearchOption searchOption) => Directory.GetFiles(path, searchPattern, searchOption); + public DirectoryInfo CreateDirectoryInfo(string path) => new(path); + + public Process? ProcessStart(string fileName, string arguments) => Process.Start(fileName, arguments); + public Process? ProcessStart(ProcessStartInfo startInfo) => Process.Start(startInfo); + } +} diff --git a/Shared/SharedCore/Shared.Core/Services/IFileSystemAccess.cs b/Shared/SharedCore/Shared.Core/Services/IFileSystemAccess.cs new file mode 100644 index 000000000..3730c45a3 --- /dev/null +++ b/Shared/SharedCore/Shared.Core/Services/IFileSystemAccess.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; + +namespace Shared.Core.Services +{ + public interface IFileSystemAccess + { + string? PathGetDirectoryName(string path); + string PathGetFileName(string path); + + void FileWriteAllBytes(string path, byte[] bytes); + bool FileExists(string path); + + bool DirectoryExists(string path); + string[] DirectoryGetFiles(string path, string searchPattern, SearchOption searchOption); + DirectoryInfo CreateDirectoryInfo(string path); + + Process? ProcessStart(string fileName, string arguments); + Process? ProcessStart(ProcessStartInfo startInfo); + } +} diff --git a/Shared/SharedCore/Shared.Core/Services/IStandardDialogs.cs b/Shared/SharedCore/Shared.Core/Services/IStandardDialogs.cs index 7fe09ca19..648c420c2 100644 --- a/Shared/SharedCore/Shared.Core/Services/IStandardDialogs.cs +++ b/Shared/SharedCore/Shared.Core/Services/IStandardDialogs.cs @@ -10,6 +10,9 @@ public interface IStandardDialogs SaveDialogResult DisplaySaveDialog(IPackFileService pfs, List extensions); BrowseDialogResultFile DisplayBrowseDialog(List extensions); BrowseDialogResultFolder DisplayBrowseFolderDialog(); + SystemOpenFileDialogResult ShowSystemOpenFileDialog(bool multiselect = false, string filter = "All files|*.*"); + SystemSaveFileDialogResult ShowSystemSaveFileDialog(string initialFileName, string filter, string defaultExt); + SystemBrowseFolderDialogResult ShowSystemFolderBrowserDialog(); string ShowFolderNameDialog(IEnumerable existingNames, string currentValue = ""); void ShowExceptionWindow(Exception e, string userInfo = ""); @@ -24,6 +27,9 @@ public interface IStandardDialogs public record SaveDialogResult(bool Result, PackFile? SelectedPackFile, string? SelectedFilePath); public record BrowseDialogResultFile(bool Result, PackFile File); public record BrowseDialogResultFolder(bool Result, string Folder); + public record SystemOpenFileDialogResult(bool Result, IReadOnlyList FilePaths); + public record SystemSaveFileDialogResult(bool Result, string? FilePath); + public record SystemBrowseFolderDialogResult(bool Result, string? FolderPath); public record TextInputDialogResult(bool Result, string Text); public enum ShowMessageBoxResult diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs index c3f6aa413..e864871ae 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs @@ -1,12 +1,11 @@ using System; using System.IO; -using System.Windows.Forms; using Shared.Core.Misc; using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class ExportToDirectoryCommand(IStandardDialogs standardDialogs) : IContextMenuCommand + public class ExportToDirectoryCommand(IStandardDialogs standardDialogs, IFileSystemAccess fileSystemAccess) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Export to system folder"; public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.Directory || node.NodeType == NodeType.Root || (node.NodeType == NodeType.File && node.Item != null); @@ -15,12 +14,12 @@ public class ExportToDirectoryCommand(IStandardDialogs standardDialogs) : IConte public void Execute(TreeNode selectedNode) { // TODO: Fix bug where if you export the packfilecontainer itself it doesn't export correctly. - using var dialog = new FolderBrowserDialog(); - if (dialog.ShowDialog() == DialogResult.OK) + var folderDialogResult = standardDialogs.ShowSystemFolderBrowserDialog(); + if (folderDialogResult.Result && !string.IsNullOrEmpty(folderDialogResult.FolderPath)) { - var nodeStartDir = Path.GetDirectoryName(selectedNode.GetFullPath()); + var nodeStartDir = fileSystemAccess.PathGetDirectoryName(selectedNode.GetFullPath()); var fileCounter = 0; - SaveSelfAndChildren(selectedNode, dialog.SelectedPath, nodeStartDir, ref fileCounter); + SaveSelfAndChildren(selectedNode, folderDialogResult.FolderPath, nodeStartDir, ref fileCounter); standardDialogs.ShowDialogBox($"{fileCounter} files exported!", "Export"); } } @@ -38,13 +37,13 @@ void SaveSelfAndChildren(TreeNode node, string outputDirectory, string rootPath, var nodePathWithoutRoot = ComputeRelativePath(nodeOriginalPath, rootPath); var fileOutputPath = outputDirectory + nodePathWithoutRoot; - var fileOutputDir = Path.GetDirectoryName(fileOutputPath); + var fileOutputDir = fileSystemAccess.PathGetDirectoryName(fileOutputPath); DirectoryHelper.EnsureCreated(fileOutputDir); var packFile = node.Item; var bytes = packFile.DataSource.ReadData(); - File.WriteAllBytes(fileOutputPath, bytes); + fileSystemAccess.FileWriteAllBytes(fileOutputPath, bytes); fileCounter++; } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs index 2e50ffb53..b60fba31f 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Windows.Forms; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.FileSources; @@ -9,7 +8,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class ImportDirectoryCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand + public class ImportDirectoryCommand(IPackFileService packFileService, IStandardDialogs standardDialogs, IFileSystemAccess fileSystemAccess) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Import Directory"; public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; @@ -23,12 +22,12 @@ public void Execute(TreeNode _selectedNode) return; } - using var dialog = new FolderBrowserDialog(); - if (dialog.ShowDialog() == DialogResult.OK) + var folderDialogResult = standardDialogs.ShowSystemFolderBrowserDialog(); + if (folderDialogResult.Result && !string.IsNullOrEmpty(folderDialogResult.FolderPath)) { - var folderPath = dialog.SelectedPath; - var folderName = new DirectoryInfo(folderPath).Name; - var originalFilePaths = Directory.GetFiles(folderPath, "*", SearchOption.AllDirectories); + var folderPath = folderDialogResult.FolderPath; + var folderName = fileSystemAccess.CreateDirectoryInfo(folderPath).Name; + var originalFilePaths = fileSystemAccess.DirectoryGetFiles(folderPath, "*", SearchOption.AllDirectories); var filePaths = originalFilePaths.Select(x => x.Replace($"{folderPath}\\", "")).ToList(); var packNodeParentPath = _selectedNode.GetFullPath(); @@ -39,7 +38,7 @@ public void Execute(TreeNode _selectedNode) for (var i = 0; i < filePaths.Count; i++) { var currentPath = filePaths[i]; - var fileName = Path.GetFileName(currentPath); + var fileName = fileSystemAccess.PathGetFileName(currentPath); var packDirectoryPath = $"{packNodeParentPath.ToLower()}{folderName}"; diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs index 6534b43bf..f6b3dbd8f 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Windows.Forms; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.FileSources; @@ -21,15 +20,11 @@ public void Execute(TreeNode _selectedNode) return; } - using var dialog = new OpenFileDialog() - { - Multiselect = true, - }; - - if (dialog.ShowDialog() == DialogResult.OK) + var dialogResult = standardDialogs.ShowSystemOpenFileDialog(multiselect: true); + if (dialogResult.Result) { var parentPath = _selectedNode.GetFullPath(); - var files = dialog.FileNames; + var files = dialogResult.FilePaths; foreach (var file in files) { var fileName = Path.GetFileName(file); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs index 84027bf55..5117febed 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInCommand.cs @@ -6,7 +6,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public abstract class OpenNodeInCommand(IStandardDialogs standardDialogs) : IContextMenuCommand + public abstract class OpenNodeInCommand(IStandardDialogs standardDialogs, IFileSystemAccess fileSystemAccess) : IContextMenuCommand { public abstract string GetDisplayName(TreeNode node); public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.File && node.Item != null; @@ -16,7 +16,7 @@ public abstract class OpenNodeInCommand(IStandardDialogs standardDialogs) : ICon protected void OpenPackFileUsing(string applicationPath, PackFile packFile) { - if (File.Exists(applicationPath) == false) + if (fileSystemAccess.FileExists(applicationPath) == false) { standardDialogs.ShowDialogBox($"Application {applicationPath} does not exist"); return; @@ -27,26 +27,26 @@ protected void OpenPackFileUsing(string applicationPath, PackFile packFile) var path = tempFolder + "\\" + fileName; var bytes = packFile.DataSource.ReadData(); - File.WriteAllBytes(path, bytes); + fileSystemAccess.FileWriteAllBytes(path, bytes); - using var process = Process.Start(applicationPath, $"\"{path}\""); + using var process = fileSystemAccess.ProcessStart(applicationPath, $"\"{path}\""); } - protected static string ResolveApplicationPath(string appRelativePath) + protected string ResolveApplicationPath(string appRelativePath) { var x64 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), appRelativePath); - if (File.Exists(x64)) return x64; + if (fileSystemAccess.FileExists(x64)) return x64; return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), appRelativePath); } } - public class OpenNodeInNotepadCommand(IStandardDialogs standardDialogs) : OpenNodeInCommand(standardDialogs) + public class OpenNodeInNotepadCommand(IStandardDialogs standardDialogs, IFileSystemAccess fileSystemAccess) : OpenNodeInCommand(standardDialogs, fileSystemAccess) { public override string GetDisplayName(TreeNode node) => "Open in Notepad++"; public override void Execute(TreeNode _selectedNode) => OpenPackFileUsing(ResolveApplicationPath(@"Notepad++\notepad++.exe"), _selectedNode.Item!); } - public class OpenNodeInHxDCommand(IStandardDialogs standardDialogs) : OpenNodeInCommand(standardDialogs) + public class OpenNodeInHxDCommand(IStandardDialogs standardDialogs, IFileSystemAccess fileSystemAccess) : OpenNodeInCommand(standardDialogs, fileSystemAccess) { public override string GetDisplayName(TreeNode node) => "Open in Hxd"; public override void Execute(TreeNode _selectedNode) => OpenPackFileUsing(ResolveApplicationPath(@"HxD\HxD.exe"), _selectedNode.Item!); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs index 4e976550f..3b6ffd6e9 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommand.cs @@ -1,11 +1,10 @@ using System.Diagnostics; -using System.IO; using Shared.Core.PackFiles; using Shared.Core.Services; namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class OpenPackInFileExplorerCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand + public class OpenPackInFileExplorerCommand(IPackFileService packFileService, IStandardDialogs standardDialogs, IFileSystemAccess fileSystemAccess) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Open In File Explorer"; public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; @@ -20,8 +19,8 @@ public void Execute(TreeNode _selectedNode) return; } - if (!Directory.Exists(systemFilePath)) - systemFilePath = Path.GetDirectoryName(systemFilePath); + if (!fileSystemAccess.DirectoryExists(systemFilePath)) + systemFilePath = fileSystemAccess.PathGetDirectoryName(systemFilePath); if (systemFilePath == null) { @@ -29,7 +28,7 @@ public void Execute(TreeNode _selectedNode) return; } - Process.Start(new ProcessStartInfo + using var process = fileSystemAccess.ProcessStart(new ProcessStartInfo { FileName = "explorer.exe", Arguments = $"\"{systemFilePath}\"", diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs index b1d713bc0..655a49ffb 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommand.cs @@ -1,5 +1,4 @@ using System; -using System.Windows.Forms; using Serilog; using Shared.Core.ErrorHandling; using Shared.Core.PackFiles; @@ -18,11 +17,8 @@ public class SaveAsPackFileContainerCommand(IPackFileService packFileService, Ap public void Execute(TreeNode _selectedNode) { - using var saveFileDialog = new SaveFileDialog(); - saveFileDialog.FileName = _selectedNode.FileOwner.Name; - saveFileDialog.Filter = "PackFile | *.pack"; - saveFileDialog.DefaultExt = "pack"; - if (saveFileDialog.ShowDialog() != DialogResult.OK) + var saveDialogResult = standardDialogs.ShowSystemSaveFileDialog(_selectedNode.FileOwner.Name, "PackFile | *.pack", "pack"); + if (!saveDialogResult.Result || string.IsNullOrEmpty(saveDialogResult.FilePath)) return; using (new WaitCursor()) @@ -30,7 +26,7 @@ public void Execute(TreeNode _selectedNode) try { var gameInformation = GameInformationDatabase.GetGameById(applicationSettingsService.CurrentSettings.CurrentGame); - packFileService.SavePackContainer(_selectedNode.FileOwner, saveFileDialog.FileName, false, gameInformation); + packFileService.SavePackContainer(_selectedNode.FileOwner, saveDialogResult.FilePath, false, gameInformation); _selectedNode.UnsavedChanged = false; _selectedNode.ForeachNode((node) => node.UnsavedChanged = false); } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs index efab1636d..c0a09c87b 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommand.cs @@ -1,5 +1,4 @@ using System; -using System.Windows.Forms; using Serilog; using Shared.Core.ErrorHandling; using Shared.Core.PackFiles; @@ -24,16 +23,13 @@ public void Execute(TreeNode _selectedNode) var systemPath = _selectedNode.FileOwner.SystemFilePath; if (string.IsNullOrWhiteSpace(systemPath)) { - using var saveFileDialog = new SaveFileDialog(); - saveFileDialog.FileName = _selectedNode.FileOwner.Name; - saveFileDialog.Filter = "PackFile | *.pack"; - saveFileDialog.DefaultExt = "pack"; - if (saveFileDialog.ShowDialog() != DialogResult.OK) + var saveDialogResult = standardDialogs.ShowSystemSaveFileDialog(_selectedNode.FileOwner.Name, "PackFile | *.pack", "pack"); + if (!saveDialogResult.Result || string.IsNullOrEmpty(saveDialogResult.FilePath)) return; - systemPath = saveFileDialog.FileName; + systemPath = saveDialogResult.FilePath; } - using (new WaitCursor()) + using (standardDialogs.ShowWaitCursor()) { try { @@ -60,16 +56,13 @@ public void Execute() var systemPath = pack.SystemFilePath; if (string.IsNullOrWhiteSpace(systemPath)) { - using var saveFileDialog = new SaveFileDialog(); - saveFileDialog.FileName = pack.Name; - saveFileDialog.Filter = "PackFile | *.pack"; - saveFileDialog.DefaultExt = "pack"; - if (saveFileDialog.ShowDialog() != DialogResult.OK) + var saveDialogResult = standardDialogs.ShowSystemSaveFileDialog(pack.Name, "PackFile | *.pack", "pack"); + if (!saveDialogResult.Result || string.IsNullOrEmpty(saveDialogResult.FilePath)) return; - systemPath = saveFileDialog.FileName; + systemPath = saveDialogResult.FilePath; } - using (new WaitCursor()) + using (standardDialogs.ShowWaitCursor()) { try { diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/StandardDialogs.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/StandardDialogs.cs index 9ef6ad4c7..608c5631d 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/StandardDialogs.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/StandardDialog/StandardDialogs.cs @@ -91,6 +91,45 @@ public BrowseDialogResultFolder DisplayBrowseFolderDialog() return output; } + public SystemOpenFileDialogResult ShowSystemOpenFileDialog(bool multiselect = false, string filter = "All files|*.*") + { + using var dialog = new System.Windows.Forms.OpenFileDialog + { + Multiselect = multiselect, + Filter = filter + }; + + if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) + return new SystemOpenFileDialogResult(true, dialog.FileNames); + + return new SystemOpenFileDialogResult(false, Array.Empty()); + } + + public SystemSaveFileDialogResult ShowSystemSaveFileDialog(string initialFileName, string filter, string defaultExt) + { + using var dialog = new System.Windows.Forms.SaveFileDialog + { + FileName = initialFileName, + Filter = filter, + DefaultExt = defaultExt + }; + + if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) + return new SystemSaveFileDialogResult(true, dialog.FileName); + + return new SystemSaveFileDialogResult(false, null); + } + + public SystemBrowseFolderDialogResult ShowSystemFolderBrowserDialog() + { + using var dialog = new System.Windows.Forms.FolderBrowserDialog(); + + if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) + return new SystemBrowseFolderDialogResult(true, dialog.SelectedPath); + + return new SystemBrowseFolderDialogResult(false, null); + } + public string ShowFolderNameDialog(IEnumerable existingNames, string currentValue = "") { var isInputCorrect = false; @@ -110,13 +149,13 @@ public string ShowFolderNameDialog(IEnumerable existingNames, string cur if (string.IsNullOrWhiteSpace(newFolderName)) { - MessageBox.Show("Folder name can not be empty! Please Try Again."); + System.Windows.MessageBox.Show("Folder name can not be empty! Please Try Again."); isInputCorrect = false; } if (isInputCorrect && normalizedExistingNames.Contains(newFolderName)) { - MessageBox.Show($"Folder with name '{newFolderName}' already exists in this folder!\nPlease Try Again."); + System.Windows.MessageBox.Show($"Folder with name '{newFolderName}' already exists in this folder!\nPlease Try Again."); isInputCorrect = false; } @@ -127,7 +166,7 @@ public string ShowFolderNameDialog(IEnumerable existingNames, string cur { if (listOfInvalidChars.Contains(c)) { - MessageBox.Show($"Folder name contains invalid character: {c}. \nPlease Try Again."); + System.Windows.MessageBox.Show($"Folder name contains invalid character: {c}. \nPlease Try Again."); isInputCorrect = false; break; } @@ -170,12 +209,12 @@ public TextInputDialogResult ShowTextInputDialog(string title, string initialTex public void ShowDialogBox(string message, string title) { - MessageBox.Show(message, title); + System.Windows.MessageBox.Show(message, title); } public ShowMessageBoxResult ShowYesNoBox(string message, string title) { - var result = MessageBox.Show(message, title, MessageBoxButton.YesNo); + var result = System.Windows.MessageBox.Show(message, title, MessageBoxButton.YesNo); if (result == MessageBoxResult.Yes) return ShowMessageBoxResult.OK; return ShowMessageBoxResult.Cancel; diff --git a/Shared/SharedUI/Shared.Ui/DependencyInjectionContainer.cs b/Shared/SharedUI/Shared.Ui/DependencyInjectionContainer.cs index 62f316f4b..286a2889e 100644 --- a/Shared/SharedUI/Shared.Ui/DependencyInjectionContainer.cs +++ b/Shared/SharedUI/Shared.Ui/DependencyInjectionContainer.cs @@ -22,6 +22,7 @@ public override void Register(IServiceCollection services) // Implement required interfaces services.AddScoped(); services.AddScoped(); + services.AddSingleton(); services.AddTransient(); services.AddScoped(); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DuplicateFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DuplicateFileCommandTests.cs deleted file mode 100644 index 7f5a3b0eb..000000000 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DuplicateFileCommandTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Threading; -using Shared.Core.PackFiles.Models; -using Shared.Core.PackFiles; -using Shared.Ui.BaseDialogs.PackFileTree; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -using Test.TestingUtility.Shared; - -namespace Shared.UiTest.ContextMenuCommands -{ - [TestFixture] - [Apartment(ApartmentState.STA)] - internal class DuplicateFileCommandTests : ContextMenuCommandTestBase - { - [Test] - public void ShouldAdd_IsEnabled_Execute() - { - var runner = new AssetEditorTestRunner(); - runner.PackFileService.EnforceGameFilesMustBeLoaded = false; - var sourcePackFile = runner.CreateEmptyPackFile("SourcePack", false); - var outputPackFile = runner.CreateEmptyPackFile("OutputPack", true); - - var fileEntry = new NewPackFileEntry("Animation\\Meta", PackFile.CreateFromASCII("testFile.anm", "DummyContent")); - runner.PackFileService.AddFilesToPack(sourcePackFile, [fileEntry]); - var fileToCopy = runner.PackFileService.FindFile("Animation\\Meta\\testFile.anm", sourcePackFile)!; - - var node = new TreeNode(fileToCopy.Name, NodeType.File, sourcePackFile, null, fileToCopy); - var command = runner.CommandFactory.Create(); - - Assert.That(command.ShouldAdd(node), Is.True); - Assert.That(command.IsEnabled(node), Is.True); - - command.Execute(node); - - var foundFile = runner.PackFileService.FindFile("Animation\\Meta\\testFile_copy.anm", outputPackFile); - Assert.That(foundFile, Is.Not.Null); - } - } -} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ClosePackContainerFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs similarity index 51% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ClosePackContainerFileCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs index da469c61b..ffb9b16ac 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ClosePackContainerFileCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs @@ -1,19 +1,39 @@ -using System.Threading; -using Moq; +using Moq; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; using Shared.Core.Services; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class ClosePackContainerFileCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueForRoot() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new ClosePackContainerFileCommand(new Mock().Object, new Mock().Object); + + Assert.That(command.ShouldAdd(root), Is.True); + Assert.That(command.ShouldAdd(file), Is.False); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new ClosePackContainerFileCommand(new Mock().Object, new Mock().Object); + + Assert.That(command.IsEnabled(root), Is.True); + } + + [Test] + public void Execute_ConfirmsAndUnloadsPack() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); @@ -23,10 +43,6 @@ public void ShouldAdd_IsEnabled_Execute() var command = new ClosePackContainerFileCommand(service.Object, dialogs.Object); - Assert.That(command.ShouldAdd(root), Is.True); - Assert.That(command.ShouldAdd(new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a"))), Is.False); - Assert.That(command.IsEnabled(root), Is.True); - command.Execute(root); service.Verify(x => x.UnloadPackContainer(owner), Times.Once); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CollapseNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs similarity index 55% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CollapseNodeCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs index 198c36e27..ff9be3b95 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CollapseNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs @@ -1,16 +1,38 @@ -using System.Threading; -using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class CollapseNodeCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueForFolderAndFalseForFile() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var folder = new TreeNode("folder", NodeType.Directory, owner, root); + var file = new TreeNode("file.txt", NodeType.File, owner, folder, PackFile.CreateFromASCII("file.txt", "a")); + var command = new CollapseNodeCommand(); + + Assert.That(command.ShouldAdd(folder), Is.True); + Assert.That(command.ShouldAdd(file), Is.False); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var folder = new TreeNode("folder", NodeType.Directory, owner, root); + var command = new CollapseNodeCommand(); + + Assert.That(command.IsEnabled(folder), Is.True); + } + + [Test] + public void Execute_CollapsesRootNode() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); @@ -27,10 +49,6 @@ public void ShouldAdd_IsEnabled_Execute() var command = new CollapseNodeCommand(); - Assert.That(command.ShouldAdd(folder), Is.True); - Assert.That(command.ShouldAdd(file), Is.False); - Assert.That(command.IsEnabled(folder), Is.True); - command.Execute(root); Assert.That(root.IsNodeExpanded, Is.False); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ContextMenuCommandTestBase.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs similarity index 85% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ContextMenuCommandTestBase.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs index a55dfa56f..e82188015 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ContextMenuCommandTestBase.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs @@ -1,8 +1,7 @@ -using Moq; +using Moq; using Shared.Core.PackFiles.Models; -using Shared.Ui.BaseDialogs.PackFileTree; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { internal abstract class ContextMenuCommandTestBase { diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyNodePathCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs similarity index 50% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyNodePathCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs index 6f7f02bf6..0773b5d5c 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyNodePathCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs @@ -1,18 +1,40 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class CopyNodePathCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueForFile() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new CopyNodePathCommand(new Mock().Object); + + Assert.That(command.ShouldAdd(file), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new CopyNodePathCommand(new Mock().Object); + + Assert.That(command.IsEnabled(file), Is.True); + } + + [Test] + [Apartment(ApartmentState.STA)] + public void Execute_CopiesNodePathToClipboard() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); @@ -22,9 +44,6 @@ public void ShouldAdd_IsEnabled_Execute() var command = new CopyNodePathCommand(service.Object); - Assert.That(command.ShouldAdd(file), Is.True); - Assert.That(command.IsEnabled(file), Is.True); - command.Execute(file); Assert.That(System.Windows.Clipboard.GetText(), Is.EqualTo("folder\\file.txt")); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyToEditablePackCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs similarity index 54% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyToEditablePackCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs index 4c8672f3c..fcf17e44b 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CopyToEditablePackCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; @@ -6,14 +6,36 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class CopyToEditablePackCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueWhenEditablePackExists() + { + var source = CreateContainer(name: "source"); + var target = CreateContainer(name: "target"); + var service = new Mock(); + service.Setup(x => x.GetEditablePack()).Returns(target); + var command = new CopyToEditablePackCommand(service.Object, new Mock().Object); + var node = new TreeNode("folder", NodeType.Directory, source, null); + + Assert.That(command.ShouldAdd(node), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var source = CreateContainer(name: "source"); + var node = new TreeNode("folder", NodeType.Directory, source, null); + var command = new CopyToEditablePackCommand(new Mock().Object, new Mock().Object); + + Assert.That(command.IsEnabled(node), Is.True); + } + + [Test] + public void Execute_CopiesChildFilesToEditablePack() { var source = CreateContainer(name: "source"); var target = CreateContainer(name: "target"); @@ -32,9 +54,6 @@ public void ShouldAdd_IsEnabled_Execute() var command = new CopyToEditablePackCommand(service.Object, dialogs.Object); - Assert.That(command.ShouldAdd(folder), Is.True); - Assert.That(command.IsEnabled(folder), Is.True); - command.Execute(folder); service.Verify(x => x.CopyFileFromOtherPackFile(source, It.IsAny(), target), Times.Once); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CreateFolderCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs similarity index 59% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CreateFolderCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs index b0c390d57..4f14966d1 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/CreateFolderCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading; using Moq; @@ -6,14 +6,33 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class CreateFolderCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueForEditableRoot() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new CreateFolderCommand(new Mock().Object); + + Assert.That(command.ShouldAdd(root), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new CreateFolderCommand(new Mock().Object); + + Assert.That(command.IsEnabled(root), Is.True); + } + + [Test] + public void Execute_AddsFolderChild() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); @@ -25,9 +44,6 @@ public void ShouldAdd_IsEnabled_Execute() var command = new CreateFolderCommand(dialogs.Object); - Assert.That(command.ShouldAdd(root), Is.True); - Assert.That(command.IsEnabled(root), Is.True); - command.Execute(root); Assert.That(root.BackingChildren.Any(x => x.Name == "new_folder"), Is.True); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DeleteNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs similarity index 53% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DeleteNodeCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs index d2248bddd..43be46754 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/DeleteNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; @@ -6,14 +6,35 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class DeleteNodeCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueForFile() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new DeleteNodeCommand(new Mock().Object, new Mock().Object); + + Assert.That(command.ShouldAdd(file), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new DeleteNodeCommand(new Mock().Object, new Mock().Object); + + Assert.That(command.IsEnabled(file), Is.True); + } + + [Test] + public void Execute_DeletesFileAfterConfirmation() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); @@ -26,9 +47,6 @@ public void ShouldAdd_IsEnabled_Execute() var command = new DeleteNodeCommand(service.Object, dialogs.Object); - Assert.That(command.ShouldAdd(file), Is.True); - Assert.That(command.IsEnabled(file), Is.True); - command.Execute(file); service.Verify(x => x.DeleteFile(owner, file.Item!), Times.Once); diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs new file mode 100644 index 000000000..0d30ca64f --- /dev/null +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs @@ -0,0 +1,64 @@ +using System.Threading; +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles; +using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; +using Test.TestingUtility.Shared; + +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +{ + [TestFixture] + internal class DuplicateFileCommandTests : ContextMenuCommandTestBase + { + [Test] + public void ShouldAdd_ReturnsTrueForFileNode() + { + var runner = new AssetEditorTestRunner(); + runner.PackFileService.EnforceGameFilesMustBeLoaded = false; + var sourcePackFile = runner.CreateEmptyPackFile("SourcePack", false); + var fileEntry = new NewPackFileEntry("Animation\\Meta", PackFile.CreateFromASCII("testFile.anm", "DummyContent")); + runner.PackFileService.AddFilesToPack(sourcePackFile, [fileEntry]); + var fileToCopy = runner.PackFileService.FindFile("Animation\\Meta\\testFile.anm", sourcePackFile)!; + var node = new TreeNode(fileToCopy.Name, NodeType.File, sourcePackFile, null, fileToCopy); + var command = runner.CommandFactory.Create(); + + Assert.That(command.ShouldAdd(node), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var runner = new AssetEditorTestRunner(); + runner.PackFileService.EnforceGameFilesMustBeLoaded = false; + var sourcePackFile = runner.CreateEmptyPackFile("SourcePack", false); + var fileEntry = new NewPackFileEntry("Animation\\Meta", PackFile.CreateFromASCII("testFile.anm", "DummyContent")); + runner.PackFileService.AddFilesToPack(sourcePackFile, [fileEntry]); + var fileToCopy = runner.PackFileService.FindFile("Animation\\Meta\\testFile.anm", sourcePackFile)!; + var node = new TreeNode(fileToCopy.Name, NodeType.File, sourcePackFile, null, fileToCopy); + var command = runner.CommandFactory.Create(); + + Assert.That(command.IsEnabled(node), Is.True); + } + + [Test] + public void Execute_DuplicatesFileIntoEditablePack() + { + var runner = new AssetEditorTestRunner(); + runner.PackFileService.EnforceGameFilesMustBeLoaded = false; + var sourcePackFile = runner.CreateEmptyPackFile("SourcePack", false); + var outputPackFile = runner.CreateEmptyPackFile("OutputPack", true); + + var fileEntry = new NewPackFileEntry("Animation\\Meta", PackFile.CreateFromASCII("testFile.anm", "DummyContent")); + runner.PackFileService.AddFilesToPack(sourcePackFile, [fileEntry]); + var fileToCopy = runner.PackFileService.FindFile("Animation\\Meta\\testFile.anm", sourcePackFile)!; + + var node = new TreeNode(fileToCopy.Name, NodeType.File, sourcePackFile, null, fileToCopy); + var command = runner.CommandFactory.Create(); + + command.Execute(node); + + var foundFile = runner.PackFileService.FindFile("Animation\\Meta\\testFile_copy.anm", outputPackFile); + Assert.That(foundFile, Is.Not.Null); + } + } +} diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExpandNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs similarity index 58% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExpandNodeCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs index 5a19e6cd7..b8868e71d 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExpandNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs @@ -1,16 +1,39 @@ -using System.Threading; +using System.Threading; using Shared.Core.PackFiles.Models; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class ExpandNodeCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueForFolderAndFalseForFile() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var folder = new TreeNode("folder", NodeType.Directory, owner, root); + var file = new TreeNode("file.txt", NodeType.File, owner, folder, PackFile.CreateFromASCII("file.txt", "a")); + var command = new ExpandNodeCommand(); + + Assert.That(command.ShouldAdd(folder), Is.True); + Assert.That(command.ShouldAdd(file), Is.False); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var folder = new TreeNode("folder", NodeType.Directory, owner, root); + var command = new ExpandNodeCommand(); + + Assert.That(command.IsEnabled(folder), Is.True); + } + + [Test] + public void Execute_ExpandsAllNodes() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); @@ -27,10 +50,6 @@ public void ShouldAdd_IsEnabled_Execute() var command = new ExpandNodeCommand(); - Assert.That(command.ShouldAdd(folder), Is.True); - Assert.That(command.ShouldAdd(file), Is.False); - Assert.That(command.IsEnabled(folder), Is.True); - command.Execute(root); Assert.That(root.IsNodeExpanded, Is.True); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExportToDirectoryCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs similarity index 63% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExportToDirectoryCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs index be057c5c8..8c7303986 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ExportToDirectoryCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs @@ -1,24 +1,40 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.Services; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class ExportToDirectoryCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled() + public void ShouldAdd_ReturnsTrueForRoot() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); - var command = new ExportToDirectoryCommand(new Mock().Object); + var command = new ExportToDirectoryCommand(new Mock().Object, new Mock().Object); Assert.That(command.ShouldAdd(root), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new ExportToDirectoryCommand(new Mock().Object, new Mock().Object); + Assert.That(command.IsEnabled(root), Is.True); + } + + [Test] + public void Execute_IgnoredUntilFilesystemPassTwo() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new ExportToDirectoryCommand(new Mock().Object, new Mock().Object); // TODO: Execute uses FolderBrowserDialog and writes exported files to disk; skip for filesystem pass two. Assert.Ignore("TODO: Execute uses FolderBrowserDialog and writes exported files to disk; skip for filesystem pass two."); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportDirectoryCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs similarity index 57% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportDirectoryCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs index 139e386fe..b003eceae 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportDirectoryCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using Moq; using Shared.Core.PackFiles; @@ -7,24 +7,37 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class ImportDirectoryCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute_CaPackShowsError() + public void ShouldAdd_ReturnsTrueForDirectoryNode() + { + var command = new ImportDirectoryCommand(new Mock().Object, new Mock().Object, new Mock().Object); + Assert.That(command.ShouldAdd(new TreeNode("dir", NodeType.Directory, CreateContainer(), null)), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(isCa: true); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new ImportDirectoryCommand(new Mock().Object, new Mock().Object, new Mock().Object); + + Assert.That(command.IsEnabled(root), Is.True); + } + + [Test] + public void Execute_CaPackShowsErrorAndDoesNotImport() { var owner = CreateContainer(isCa: true); var root = new TreeNode("root", NodeType.Root, owner, null); var service = new Mock(); var dialogs = new Mock(); - var command = new ImportDirectoryCommand(service.Object, dialogs.Object); - - Assert.That(command.ShouldAdd(new TreeNode("dir", NodeType.Directory, CreateContainer(), null)), Is.True); - Assert.That(command.IsEnabled(root), Is.True); + var command = new ImportDirectoryCommand(service.Object, dialogs.Object, new Mock().Object); command.Execute(root); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs similarity index 61% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportFileCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs index 1e8c18960..30fb34bfe 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/ImportFileCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using Moq; using Shared.Core.PackFiles; @@ -7,14 +7,30 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class ImportFileCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute_CaPackShowsError() + public void ShouldAdd_ReturnsTrueForDirectoryNode() + { + var command = new ImportFileCommand(new Mock().Object, new Mock().Object); + Assert.That(command.ShouldAdd(new TreeNode("dir", NodeType.Directory, CreateContainer(), null)), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(isCa: true); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new ImportFileCommand(new Mock().Object, new Mock().Object); + + Assert.That(command.IsEnabled(root), Is.True); + } + + [Test] + public void Execute_CaPackShowsErrorAndDoesNotImport() { var owner = CreateContainer(isCa: true); var root = new TreeNode("root", NodeType.Root, owner, null); @@ -23,9 +39,6 @@ public void ShouldAdd_IsEnabled_Execute_CaPackShowsError() var dialogs = new Mock(); var command = new ImportFileCommand(service.Object, dialogs.Object); - Assert.That(command.ShouldAdd(new TreeNode("dir", NodeType.Directory, CreateContainer(), null)), Is.True); - Assert.That(command.IsEnabled(root), Is.True); - command.Execute(root); dialogs.Verify(x => x.ShowDialogBox("Unable to edit CA packfile", "Error"), Times.Once); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInHxDCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs similarity index 57% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInHxDCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs index 0fb5a0afe..7c05a2246 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInHxDCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs @@ -1,27 +1,40 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles.Models; using Shared.Core.Services; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class OpenNodeInHxDCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled() + public void ShouldAdd_ReturnsTrueForFileNode() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); - var command = new OpenNodeInHxDCommand(new Mock().Object); + var command = new OpenNodeInHxDCommand(new Mock().Object, new Mock().Object); Assert.That(command.ShouldAdd(file), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new OpenNodeInHxDCommand(new Mock().Object, new Mock().Object); + Assert.That(command.IsEnabled(file), Is.True); + } + [Test] + public void Execute_IgnoredUntilExternalProcessPassTwo() + { // TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass. Assert.Ignore("TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass."); } diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInNotepadCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs similarity index 57% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInNotepadCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs index c282677f4..b237b944f 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenNodeInNotepadCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs @@ -1,27 +1,40 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles.Models; using Shared.Core.Services; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class OpenNodeInNotepadCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled() + public void ShouldAdd_ReturnsTrueForFileNode() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); - var command = new OpenNodeInNotepadCommand(new Mock().Object); + var command = new OpenNodeInNotepadCommand(new Mock().Object, new Mock().Object); Assert.That(command.ShouldAdd(file), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new OpenNodeInNotepadCommand(new Mock().Object, new Mock().Object); + Assert.That(command.IsEnabled(file), Is.True); + } + [Test] + public void Execute_IgnoredUntilExternalProcessPassTwo() + { // TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass. Assert.Ignore("TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass."); } diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenPackInFileExplorerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs similarity index 65% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenPackInFileExplorerCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs index 8fe449a5d..1acdff2c5 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/OpenPackInFileExplorerCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs @@ -1,26 +1,38 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles; using Shared.Core.Services; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class OpenPackInFileExplorerCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled() + public void ShouldAdd_ReturnsTrueForRoot() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); - var command = new OpenPackInFileExplorerCommand(new Mock().Object, new Mock().Object); + var command = new OpenPackInFileExplorerCommand(new Mock().Object, new Mock().Object, new Mock().Object); Assert.That(command.ShouldAdd(root), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new OpenPackInFileExplorerCommand(new Mock().Object, new Mock().Object, new Mock().Object); + Assert.That(command.IsEnabled(root), Is.True); + } + [Test] + public void Execute_IgnoredUntilExternalProcessPassTwo() + { // TODO: Execute launches explorer.exe (external process); skip in first pass. Assert.Ignore("TODO: Execute launches explorer.exe (external process); skip in first pass."); } @@ -31,7 +43,7 @@ public void Execute_NullSystemFilePath_ShowsError() var owner = CreateContainer(systemFilePath: ""); var root = new TreeNode("root", NodeType.Root, owner, null); var dialogs = new Mock(); - var command = new OpenPackInFileExplorerCommand(new Mock().Object, dialogs.Object); + var command = new OpenPackInFileExplorerCommand(new Mock().Object, dialogs.Object, new Mock().Object); command.Execute(root); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/RenameNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs similarity index 54% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/RenameNodeCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs index a80d2f908..4245a6eee 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/RenameNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; @@ -6,14 +6,35 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class RenameNodeCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueForFile() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new RenameNodeCommand(new Mock().Object, new Mock().Object); + + Assert.That(command.ShouldAdd(file), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + var command = new RenameNodeCommand(new Mock().Object, new Mock().Object); + + Assert.That(command.IsEnabled(file), Is.True); + } + + [Test] + public void Execute_RenamesFile() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); @@ -25,9 +46,6 @@ public void ShouldAdd_IsEnabled_Execute() dialogs.Setup(x => x.ShowTextInputDialog("Rename file", file.Name)).Returns(new TextInputDialogResult(true, "renamed.txt")); var command = new RenameNodeCommand(service.Object, dialogs.Object); - Assert.That(command.ShouldAdd(file), Is.True); - Assert.That(command.IsEnabled(file), Is.True); - command.Execute(file); service.Verify(x => x.RenameFile(owner, file.Item!, "renamed.txt"), Times.Once); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SaveAsPackFileContainerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs similarity index 62% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SaveAsPackFileContainerCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs index 7b77a8ba2..b69d9f08b 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SaveAsPackFileContainerCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles; using Shared.Core.Services; @@ -6,22 +6,34 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class SaveAsPackFileContainerCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled() + public void ShouldAdd_ReturnsTrueForRoot() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); var command = new SaveAsPackFileContainerCommand(new Mock().Object, new ApplicationSettingsService(GameTypeEnum.Warhammer3), new Mock().Object); Assert.That(command.ShouldAdd(root), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new SaveAsPackFileContainerCommand(new Mock().Object, new ApplicationSettingsService(GameTypeEnum.Warhammer3), new Mock().Object); + Assert.That(command.IsEnabled(root), Is.True); + } + [Test] + public void Execute_IgnoredUntilFilesystemPassTwo() + { // TODO: Execute uses SaveFileDialog and writes pack file to disk; skip for filesystem pass two. Assert.Ignore("TODO: Execute uses SaveFileDialog and writes pack file to disk; skip for filesystem pass two."); } diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SavePackFileContainerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs similarity index 52% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SavePackFileContainerCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs index c67545474..fc6f257f9 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SavePackFileContainerCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles; using Shared.Core.Services; @@ -6,14 +6,33 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class SavePackFileContainerCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueForRoot() + { + var owner = CreateContainer(systemFilePath: "C:\\temp\\pack.pack"); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new SavePackFileContainerCommand(new Mock().Object, new Mock().Object, new ApplicationSettingsService(GameTypeEnum.Warhammer3)); + + Assert.That(command.ShouldAdd(root), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(systemFilePath: "C:\\temp\\pack.pack"); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new SavePackFileContainerCommand(new Mock().Object, new Mock().Object, new ApplicationSettingsService(GameTypeEnum.Warhammer3)); + + Assert.That(command.IsEnabled(root), Is.True); + } + + [Test] + public void Execute_SavesPackContainer() { var owner = CreateContainer(systemFilePath: "C:\\temp\\pack.pack"); var root = new TreeNode("root", NodeType.Root, owner, null); @@ -23,9 +42,6 @@ public void ShouldAdd_IsEnabled_Execute() var command = new SavePackFileContainerCommand(service.Object, dialogs.Object, appSettings); - Assert.That(command.ShouldAdd(root), Is.True); - Assert.That(command.IsEnabled(root), Is.True); - command.Execute(root); service.Verify(x => x.SavePackContainer(owner, owner.SystemFilePath, false, It.IsAny()), Times.Once); diff --git a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SetAsEditablePackCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs similarity index 55% rename from Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SetAsEditablePackCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs index 183eeabf8..bc2d543e4 100644 --- a/Shared/SharedUI/Shared.UiTest/ContextMenuCommands/SetAsEditablePackCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs @@ -1,27 +1,43 @@ -using System.Threading; +using System.Threading; using Moq; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.ContextMenuCommands +namespace Shared.UiTest.PackFileTree.ContextMenu.Commands { [TestFixture] - [Apartment(ApartmentState.STA)] internal class SetAsEditablePackCommandTests : ContextMenuCommandTestBase { [Test] - public void ShouldAdd_IsEnabled_Execute() + public void ShouldAdd_ReturnsTrueForRoot() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); - var service = new Mock(); - service.Setup(x => x.GetEditablePack()).Returns((IPackFileContainer?)null); - var command = new SetAsEditablePackCommand(service.Object); + var command = new SetAsEditablePackCommand(new Mock().Object); Assert.That(command.ShouldAdd(root), Is.True); + } + + [Test] + public void IsEnabled_ReturnsTrue() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var command = new SetAsEditablePackCommand(new Mock().Object); + Assert.That(command.IsEnabled(root), Is.True); + } + + [Test] + public void Execute_SetsEditablePack() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var service = new Mock(); + service.Setup(x => x.GetEditablePack()).Returns((IPackFileContainer?)null); + var command = new SetAsEditablePackCommand(service.Object); command.Execute(root); diff --git a/Shared/SharedUI/Shared.UiTest/Shared.UiTest.csproj b/Shared/SharedUI/Shared.UiTest/Shared.UiTest.csproj index 9bed98bbe..07cd68640 100644 --- a/Shared/SharedUI/Shared.UiTest/Shared.UiTest.csproj +++ b/Shared/SharedUI/Shared.UiTest/Shared.UiTest.csproj @@ -32,4 +32,8 @@ + + + + From e1eb966719893ffe3aef6f5e4b7ff37e666a6e33 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 15:14:43 +0200 Subject: [PATCH 19/26] Code --- .../Shared.Core/Services/FileSystemAccess.cs | 1 + .../Shared.Core/Services/IFileSystemAccess.cs | 1 + .../Commands/CollapseNodeCommand.cs | 2 +- .../ContextMenu/Commands/ExpandNodeCommand.cs | 2 +- .../Commands/ImportDirectoryCommand.cs | 2 +- .../Commands/ImportFilesCommand.cs | 4 +- .../Commands/CollapseNodeCommandTests.cs | 23 ++++++++ .../Commands/ExpandNodeCommandTests.cs | 23 ++++++++ .../Commands/ExportToDirectoryCommandTests.cs | 11 ++-- .../Commands/ImportDirectoryCommandTests.cs | 54 +++++++++++++++++++ .../Commands/ImportFileCommandTests.cs | 51 ++++++++++++++++-- .../Commands/OpenNodeInHxDCommandTests.cs | 35 ++++++++++-- .../Commands/OpenNodeInNotepadCommandTests.cs | 35 ++++++++++-- .../OpenPackInFileExplorerCommandTests.cs | 15 ++++-- .../SaveAsPackFileContainerCommandTests.cs | 16 ++++-- 15 files changed, 252 insertions(+), 23 deletions(-) diff --git a/Shared/SharedCore/Shared.Core/Services/FileSystemAccess.cs b/Shared/SharedCore/Shared.Core/Services/FileSystemAccess.cs index 3e3053bff..487a45615 100644 --- a/Shared/SharedCore/Shared.Core/Services/FileSystemAccess.cs +++ b/Shared/SharedCore/Shared.Core/Services/FileSystemAccess.cs @@ -9,6 +9,7 @@ public class FileSystemAccess : IFileSystemAccess public string PathGetFileName(string path) => Path.GetFileName(path); public void FileWriteAllBytes(string path, byte[] bytes) => File.WriteAllBytes(path, bytes); + public byte[] FileReadAllBytes(string path) => File.ReadAllBytes(path); public bool FileExists(string path) => File.Exists(path); public bool DirectoryExists(string path) => Directory.Exists(path); diff --git a/Shared/SharedCore/Shared.Core/Services/IFileSystemAccess.cs b/Shared/SharedCore/Shared.Core/Services/IFileSystemAccess.cs index 3730c45a3..066d94640 100644 --- a/Shared/SharedCore/Shared.Core/Services/IFileSystemAccess.cs +++ b/Shared/SharedCore/Shared.Core/Services/IFileSystemAccess.cs @@ -8,6 +8,7 @@ public interface IFileSystemAccess string PathGetFileName(string path); void FileWriteAllBytes(string path, byte[] bytes); + byte[] FileReadAllBytes(string path); bool FileExists(string path); bool DirectoryExists(string path); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommand.cs index 43cd779a7..03c1be15d 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommand.cs @@ -11,7 +11,7 @@ public class CollapseNodeCommand() : IContextMenuCommand void CollapsAllRecursive(TreeNode node) { node.IsNodeExpanded = false; - foreach (var child in node.Children) + foreach (var child in node.BackingChildren) CollapsAllRecursive(child); } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommand.cs index 2a938760c..7ff64a3ad 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommand.cs @@ -11,7 +11,7 @@ public class ExpandNodeCommand() : IContextMenuCommand void ExpandAllRecursive(TreeNode node) { node.IsNodeExpanded = true; - foreach (var child in node.Children) + foreach (var child in node.BackingChildren) ExpandAllRecursive(child); } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs index b60fba31f..08283b8eb 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs @@ -49,7 +49,7 @@ public void Execute(TreeNode _selectedNode) packDirectoryPath = $"{packDirectoryPath}\\{directoryPath}"; } - var source = MemorySource.FromFile(originalFilePaths[i]); + var source = new MemorySource(fileSystemAccess.FileReadAllBytes(originalFilePaths[i])); var file = new PackFile(fileName, source); filesAdded.Add(new NewPackFileEntry(packDirectoryPath, file)); diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs index f6b3dbd8f..68374f0ae 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFilesCommand.cs @@ -6,7 +6,7 @@ namespace Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands { - public class ImportFileCommand(IPackFileService packFileService, IStandardDialogs standardDialogs) : IContextMenuCommand + public class ImportFileCommand(IPackFileService packFileService, IStandardDialogs standardDialogs, IFileSystemAccess fileSystemAccess) : IContextMenuCommand { public string GetDisplayName(TreeNode node) => "Import File"; public bool ShouldAdd(TreeNode node) => node.NodeType != NodeType.File && !node.FileOwner.IsCaPackFile; @@ -28,7 +28,7 @@ public void Execute(TreeNode _selectedNode) foreach (var file in files) { var fileName = Path.GetFileName(file); - var packFile = new PackFile(fileName, new MemorySource(File.ReadAllBytes(file))); + var packFile = new PackFile(fileName, new MemorySource(fileSystemAccess.FileReadAllBytes(file))); var item = new NewPackFileEntry(parentPath, packFile); packFileService.AddFilesToPack(_selectedNode.FileOwner, [item]); } diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs index ff9be3b95..05be0882c 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs @@ -53,5 +53,28 @@ public void Execute_CollapsesRootNode() Assert.That(root.IsNodeExpanded, Is.False); } + + [Test] + public void Execute_CollapsesUnmaterializedChildren() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var folder = new TreeNode("folder", NodeType.Directory, owner, root); + var nested = new TreeNode("nested", NodeType.Directory, owner, folder); + + root.AddChild(folder); + folder.AddChild(nested); + + root.IsNodeExpanded = true; + folder.IsNodeExpanded = true; + nested.IsNodeExpanded = true; + + var command = new CollapseNodeCommand(); + command.Execute(root); + + Assert.That(root.IsNodeExpanded, Is.False); + Assert.That(folder.IsNodeExpanded, Is.False); + Assert.That(nested.IsNodeExpanded, Is.False); + } } } diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs index b8868e71d..c460cbe9b 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs @@ -56,5 +56,28 @@ public void Execute_ExpandsAllNodes() Assert.That(folder.IsNodeExpanded, Is.True); Assert.That(file.IsNodeExpanded, Is.True); } + + [Test] + public void Execute_ExpandsUnmaterializedChildren() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var folder = new TreeNode("folder", NodeType.Directory, owner, root); + var nested = new TreeNode("nested", NodeType.Directory, owner, folder); + + root.AddChild(folder); + folder.AddChild(nested); + + root.IsNodeExpanded = false; + folder.IsNodeExpanded = false; + nested.IsNodeExpanded = false; + + var command = new ExpandNodeCommand(); + command.Execute(root); + + Assert.That(root.IsNodeExpanded, Is.True); + Assert.That(folder.IsNodeExpanded, Is.True); + Assert.That(nested.IsNodeExpanded, Is.True); + } } } diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs index 8c7303986..eb430c1f5 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs @@ -34,10 +34,15 @@ public void Execute_IgnoredUntilFilesystemPassTwo() { var owner = CreateContainer(); var root = new TreeNode("root", NodeType.Root, owner, null); - var command = new ExportToDirectoryCommand(new Mock().Object, new Mock().Object); + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowSystemFolderBrowserDialog()).Returns(new SystemBrowseFolderDialogResult(false, null)); + var fileSystem = new Mock(); + var command = new ExportToDirectoryCommand(dialogs.Object, fileSystem.Object); + + command.Execute(root); - // TODO: Execute uses FolderBrowserDialog and writes exported files to disk; skip for filesystem pass two. - Assert.Ignore("TODO: Execute uses FolderBrowserDialog and writes exported files to disk; skip for filesystem pass two."); + fileSystem.Verify(x => x.FileWriteAllBytes(It.IsAny(), It.IsAny()), Times.Never); + dialogs.Verify(x => x.ShowDialogBox(It.Is(s => s.Contains("exported")), It.IsAny()), Times.Never); } [Test] diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs index b003eceae..4d0b2b084 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading; using Moq; using Shared.Core.PackFiles; @@ -44,5 +45,58 @@ public void Execute_CaPackShowsErrorAndDoesNotImport() dialogs.Verify(x => x.ShowDialogBox("Unable to edit CA packfile", "Error"), Times.Once); service.Verify(x => x.AddFilesToPack(It.IsAny(), It.IsAny>()), Times.Never); } + + [Test] + public void Execute_DialogCancelled_DoesNotImport() + { + var owner = CreateContainer(isCa: false); + var root = new TreeNode("root", NodeType.Root, owner, null); + + var service = new Mock(); + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowSystemFolderBrowserDialog()) + .Returns(new SystemBrowseFolderDialogResult(Result: false, FolderPath: string.Empty)); + var fileSystem = new Mock(); + var command = new ImportDirectoryCommand(service.Object, dialogs.Object, fileSystem.Object); + + command.Execute(root); + + service.Verify(x => x.AddFilesToPack(It.IsAny(), It.IsAny>()), Times.Never); + fileSystem.Verify(x => x.FileReadAllBytes(It.IsAny()), Times.Never); + } + + [Test] + public void Execute_DirectorySelected_ImportsFilesWithMockedReads() + { + var owner = CreateContainer(isCa: false); + var root = new TreeNode("root", NodeType.Root, owner, null); + + var folderPath = "C:\\test\\folder"; + var file1Path = "C:\\test\\folder\\file1.txt"; + var file2Path = "C:\\test\\folder\\subdir\\file2.txt"; + var file1Bytes = new byte[] { 0x01, 0x02 }; + var file2Bytes = new byte[] { 0x03, 0x04, 0x05 }; + + var service = new Mock(); + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowSystemFolderBrowserDialog()) + .Returns(new SystemBrowseFolderDialogResult(Result: true, FolderPath: folderPath)); + var fileSystem = new Mock(); + fileSystem.Setup(x => x.CreateDirectoryInfo(folderPath)).Returns(new DirectoryInfo(folderPath)); + fileSystem.Setup(x => x.DirectoryGetFiles(folderPath, "*", SearchOption.AllDirectories)) + .Returns([file1Path, file2Path]); + fileSystem.Setup(x => x.PathGetFileName(file1Path)).Returns("file1.txt"); + fileSystem.Setup(x => x.PathGetFileName(file2Path)).Returns("file2.txt"); + fileSystem.Setup(x => x.FileReadAllBytes(file1Path)).Returns(file1Bytes); + fileSystem.Setup(x => x.FileReadAllBytes(file2Path)).Returns(file2Bytes); + var command = new ImportDirectoryCommand(service.Object, dialogs.Object, fileSystem.Object); + + command.Execute(root); + + fileSystem.Verify(x => x.FileReadAllBytes(file1Path), Times.Once); + fileSystem.Verify(x => x.FileReadAllBytes(file2Path), Times.Once); + service.Verify(x => x.AddFilesToPack(owner, It.Is>(items => + items.Count == 2)), Times.Once); + } } } diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs index 30fb34bfe..8986f91ea 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading; using Moq; using Shared.Core.PackFiles; @@ -15,7 +16,7 @@ internal class ImportFileCommandTests : ContextMenuCommandTestBase [Test] public void ShouldAdd_ReturnsTrueForDirectoryNode() { - var command = new ImportFileCommand(new Mock().Object, new Mock().Object); + var command = new ImportFileCommand(new Mock().Object, new Mock().Object, new Mock().Object); Assert.That(command.ShouldAdd(new TreeNode("dir", NodeType.Directory, CreateContainer(), null)), Is.True); } @@ -24,7 +25,7 @@ public void IsEnabled_ReturnsTrue() { var owner = CreateContainer(isCa: true); var root = new TreeNode("root", NodeType.Root, owner, null); - var command = new ImportFileCommand(new Mock().Object, new Mock().Object); + var command = new ImportFileCommand(new Mock().Object, new Mock().Object, new Mock().Object); Assert.That(command.IsEnabled(root), Is.True); } @@ -37,12 +38,56 @@ public void Execute_CaPackShowsErrorAndDoesNotImport() var service = new Mock(); var dialogs = new Mock(); - var command = new ImportFileCommand(service.Object, dialogs.Object); + var fileSystem = new Mock(); + var command = new ImportFileCommand(service.Object, dialogs.Object, fileSystem.Object); command.Execute(root); dialogs.Verify(x => x.ShowDialogBox("Unable to edit CA packfile", "Error"), Times.Once); service.Verify(x => x.AddFilesToPack(It.IsAny(), It.IsAny>()), Times.Never); } + + [Test] + public void Execute_DialogCancelled_DoesNotImport() + { + var owner = CreateContainer(isCa: false); + var root = new TreeNode("root", NodeType.Root, owner, null); + + var service = new Mock(); + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowSystemOpenFileDialog(It.IsAny(), It.IsAny())) + .Returns(new SystemOpenFileDialogResult(Result: false, FilePaths: [])); + var fileSystem = new Mock(); + var command = new ImportFileCommand(service.Object, dialogs.Object, fileSystem.Object); + + command.Execute(root); + + service.Verify(x => x.AddFilesToPack(It.IsAny(), It.IsAny>()), Times.Never); + fileSystem.Verify(x => x.FileReadAllBytes(It.IsAny()), Times.Never); + } + + [Test] + public void Execute_FileSelected_ImportsFileWithMockedRead() + { + var owner = CreateContainer(isCa: false); + var root = new TreeNode("root", NodeType.Root, owner, null); + + var fileBytes = new byte[] { 0x01, 0x02, 0x03 }; + var filePath = "C:\\test\\file.txt"; + + var service = new Mock(); + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowSystemOpenFileDialog(It.IsAny(), It.IsAny())) + .Returns(new SystemOpenFileDialogResult(Result: true, FilePaths: [filePath])); + var fileSystem = new Mock(); + fileSystem.Setup(x => x.FileReadAllBytes(filePath)).Returns(fileBytes); + var command = new ImportFileCommand(service.Object, dialogs.Object, fileSystem.Object); + + command.Execute(root); + + fileSystem.Verify(x => x.FileReadAllBytes(filePath), Times.Once); + service.Verify(x => x.AddFilesToPack(owner, It.Is>(items => + items.Count == 1)), Times.Once); + } } } diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs index 7c05a2246..7f0d39f34 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs @@ -33,10 +33,39 @@ public void IsEnabled_ReturnsTrue() } [Test] - public void Execute_IgnoredUntilExternalProcessPassTwo() + public void Execute_AppMissing_ShowsError() { - // TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass. - Assert.Ignore("TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass."); + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + + var dialogs = new Mock(); + var fileSystem = new Mock(); + fileSystem.Setup(x => x.FileExists(It.IsAny())).Returns(false); + + var command = new OpenNodeInHxDCommand(dialogs.Object, fileSystem.Object); + command.Execute(file); + + dialogs.Verify(x => x.ShowDialogBox(It.Is(s => s.Contains("does not exist")), It.IsAny()), Times.Once); + fileSystem.Verify(x => x.ProcessStart(It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public void Execute_AppExists_WritesTempFileAndStartsProcess() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "abc")); + + var dialogs = new Mock(); + var fileSystem = new Mock(); + fileSystem.Setup(x => x.FileExists(It.IsAny())).Returns(true); + + var command = new OpenNodeInHxDCommand(dialogs.Object, fileSystem.Object); + command.Execute(file); + + fileSystem.Verify(x => x.FileWriteAllBytes(It.IsAny(), It.Is(b => b.Length == 3)), Times.Once); + fileSystem.Verify(x => x.ProcessStart(It.IsAny(), It.IsAny()), Times.Once); } } } diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs index b237b944f..d8c7cd34c 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs @@ -33,10 +33,39 @@ public void IsEnabled_ReturnsTrue() } [Test] - public void Execute_IgnoredUntilExternalProcessPassTwo() + public void Execute_AppMissing_ShowsError() { - // TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass. - Assert.Ignore("TODO: Execute may start external process and write temp files depending on local tool installation; skip in first pass."); + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "a")); + + var dialogs = new Mock(); + var fileSystem = new Mock(); + fileSystem.Setup(x => x.FileExists(It.IsAny())).Returns(false); + + var command = new OpenNodeInNotepadCommand(dialogs.Object, fileSystem.Object); + command.Execute(file); + + dialogs.Verify(x => x.ShowDialogBox(It.Is(s => s.Contains("does not exist")), It.IsAny()), Times.Once); + fileSystem.Verify(x => x.ProcessStart(It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public void Execute_AppExists_WritesTempFileAndStartsProcess() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var file = new TreeNode("file.txt", NodeType.File, owner, root, PackFile.CreateFromASCII("file.txt", "abc")); + + var dialogs = new Mock(); + var fileSystem = new Mock(); + fileSystem.Setup(x => x.FileExists(It.IsAny())).Returns(true); + + var command = new OpenNodeInNotepadCommand(dialogs.Object, fileSystem.Object); + command.Execute(file); + + fileSystem.Verify(x => x.FileWriteAllBytes(It.IsAny(), It.Is(b => b.Length == 3)), Times.Once); + fileSystem.Verify(x => x.ProcessStart(It.IsAny(), It.IsAny()), Times.Once); } } } diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs index 1acdff2c5..1447e4aa1 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs @@ -1,4 +1,5 @@ using System.Threading; +using System.Diagnostics; using Moq; using Shared.Core.PackFiles; using Shared.Core.Services; @@ -31,10 +32,18 @@ public void IsEnabled_ReturnsTrue() } [Test] - public void Execute_IgnoredUntilExternalProcessPassTwo() + public void Execute_ValidPath_StartsExplorer() { - // TODO: Execute launches explorer.exe (external process); skip in first pass. - Assert.Ignore("TODO: Execute launches explorer.exe (external process); skip in first pass."); + var owner = CreateContainer(systemFilePath: "C:\\temp\\pack.pack"); + var root = new TreeNode("root", NodeType.Root, owner, null); + var fileSystem = new Mock(); + fileSystem.Setup(x => x.DirectoryExists(It.IsAny())).Returns(false); + fileSystem.Setup(x => x.PathGetDirectoryName(It.IsAny())).Returns("C:\\temp"); + + var command = new OpenPackInFileExplorerCommand(new Mock().Object, new Mock().Object, fileSystem.Object); + command.Execute(root); + + fileSystem.Verify(x => x.ProcessStart(It.Is(p => p.FileName == "explorer.exe")), Times.Once); } [Test] diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs index b69d9f08b..c890bf06a 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs @@ -1,6 +1,7 @@ using System.Threading; using Moq; using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; using Shared.Core.Services; using Shared.Core.Settings; using Shared.Ui.BaseDialogs.PackFileTree; @@ -32,10 +33,19 @@ public void IsEnabled_ReturnsTrue() } [Test] - public void Execute_IgnoredUntilFilesystemPassTwo() + public void Execute_SaveDialogCancelled_DoesNotSave() { - // TODO: Execute uses SaveFileDialog and writes pack file to disk; skip for filesystem pass two. - Assert.Ignore("TODO: Execute uses SaveFileDialog and writes pack file to disk; skip for filesystem pass two."); + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + var service = new Mock(); + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowSystemSaveFileDialog(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new SystemSaveFileDialogResult(false, null)); + + var command = new SaveAsPackFileContainerCommand(service.Object, new ApplicationSettingsService(GameTypeEnum.Warhammer3), dialogs.Object); + command.Execute(root); + + service.Verify(x => x.SavePackContainer(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } } } From 85bf3dd640352ac5c269bbb6d9642e4a9b6e734b Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 15:29:00 +0200 Subject: [PATCH 20/26] Bug fix --- .../Commands/ExportToDirectoryCommand.cs | 14 +++-- .../Commands/ExportToDirectoryCommandTests.cs | 56 ++++++++++++++++++- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs index e864871ae..682ff3b92 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommand.cs @@ -17,18 +17,21 @@ public void Execute(TreeNode selectedNode) var folderDialogResult = standardDialogs.ShowSystemFolderBrowserDialog(); if (folderDialogResult.Result && !string.IsNullOrEmpty(folderDialogResult.FolderPath)) { - var nodeStartDir = fileSystemAccess.PathGetDirectoryName(selectedNode.GetFullPath()); + // For root nodes, use empty string as base path; for others, use parent directory + var nodeStartDir = selectedNode.NodeType == NodeType.Root + ? "" + : fileSystemAccess.PathGetDirectoryName(selectedNode.GetFullPath()); var fileCounter = 0; SaveSelfAndChildren(selectedNode, folderDialogResult.FolderPath, nodeStartDir, ref fileCounter); standardDialogs.ShowDialogBox($"{fileCounter} files exported!", "Export"); } } - void SaveSelfAndChildren(TreeNode node, string outputDirectory, string rootPath, ref int fileCounter) + void SaveSelfAndChildren(TreeNode node, string outputDirectory, string? rootPath, ref int fileCounter) { - if (node.NodeType == NodeType.Directory) + if (node.NodeType == NodeType.Directory || node.NodeType == NodeType.Root) { - foreach (var item in node.Children) + foreach (var item in node.BackingChildren) SaveSelfAndChildren(item, outputDirectory, rootPath, ref fileCounter); } else @@ -38,7 +41,8 @@ void SaveSelfAndChildren(TreeNode node, string outputDirectory, string rootPath, var fileOutputPath = outputDirectory + nodePathWithoutRoot; var fileOutputDir = fileSystemAccess.PathGetDirectoryName(fileOutputPath); - DirectoryHelper.EnsureCreated(fileOutputDir); + if (!string.IsNullOrEmpty(fileOutputDir)) + DirectoryHelper.EnsureCreated(fileOutputDir); var packFile = node.Item; var bytes = packFile.DataSource.ReadData(); diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs index eb430c1f5..ac0b60906 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs @@ -1,5 +1,8 @@ -using System.Threading; +using System.IO; +using System.Threading; using Moq; +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Models.FileSources; using Shared.Core.Services; using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; @@ -59,5 +62,56 @@ public void ComputeRelativePath_NullRootReturnsFullPath() var result = ExportToDirectoryCommand.ComputeRelativePath("sub\\file.mesh", null); Assert.That(result, Is.EqualTo("\\sub\\file.mesh")); } + + [Test] + public void Execute_ExportMultipleFilesFromRoot_ExportsSuccessfully() + { + var owner = CreateContainer(); + var root = new TreeNode("root", NodeType.Root, owner, null); + + // Create directory structure: root -> [dir -> file1, file2] + var dir = new TreeNode("models", NodeType.Directory, owner, root); + + // Create real PackFile objects with MemorySource + var packFile1 = new PackFile("mesh1.mesh", new MemorySource(new byte[] { 0x01, 0x02 })); + var packFile2 = new PackFile("mesh2.mesh", new MemorySource(new byte[] { 0x03, 0x04 })); + + var file1 = new TreeNode("mesh1.mesh", NodeType.File, owner, dir, packFile1); + var file2 = new TreeNode("mesh2.mesh", NodeType.File, owner, dir, packFile2); + + dir.AddChild(file1); + dir.AddChild(file2); + root.AddChild(dir); + root.MarkChildrenLoaded(); + dir.MarkChildrenLoaded(); + + var outputDir = "C:\\export"; + var dialogs = new Mock(); + dialogs.Setup(x => x.ShowSystemFolderBrowserDialog()) + .Returns(new SystemBrowseFolderDialogResult(true, outputDir)); + + var fileSystem = new Mock(); + // Mock PathGetDirectoryName to extract directory from path + fileSystem.Setup(x => x.PathGetDirectoryName(It.IsAny())) + .Returns(p => { + if (string.IsNullOrEmpty(p)) return null; + var lastSlash = p.LastIndexOf('\\'); + return lastSlash > 0 ? p.Substring(0, lastSlash) : null; + }); + fileSystem.Setup(x => x.PathGetFileName(It.IsAny())) + .Returns(p => { + if (string.IsNullOrEmpty(p)) return p; + var lastSlash = p.LastIndexOf('\\'); + return lastSlash >= 0 ? p.Substring(lastSlash + 1) : p; + }); + + var command = new ExportToDirectoryCommand(dialogs.Object, fileSystem.Object); + + command.Execute(root); + + // Verify files were written + fileSystem.Verify(x => x.FileWriteAllBytes(It.IsAny(), It.IsAny()), Times.Exactly(2)); + dialogs.Verify(x => x.ShowDialogBox("2 files exported!", "Export"), Times.Once); + } } } From f081432c4b285dafbd8f22d8ad65fd664900e310 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 19:13:52 +0200 Subject: [PATCH 21/26] Warning fixes --- .../DependencyInjection/ScopeRepository.cs | 13 ++++++++++--- .../DevConfig/DevelopmentConfigurationManager.cs | 2 +- .../ErrorHandling/ApplicationStateRecorder.cs | 4 ++-- .../Shared.Core/ErrorHandling/ErrorList.cs | 6 +++--- Shared/SharedCore/Shared.Core/Events/EventHub.cs | 5 ++++- .../Events/Global/OpenEditorCommand.cs | 1 + .../Shared.Core/Misc/DirectoryHelper.cs | 16 +++++----------- .../SharedCore/Shared.Core/Misc/FileWatcher.cs | 4 ++-- .../SharedCore/Shared.Core/Misc/JsonHelpers.cs | 8 ++------ Shared/SharedCore/Shared.Core/Misc/MathUtil.cs | 2 +- .../Shared.Core/Settings/ThemesController.cs | 3 ++- 11 files changed, 33 insertions(+), 31 deletions(-) diff --git a/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs b/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs index 8e409ffe0..7d9974f52 100644 --- a/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs +++ b/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Reflection; using System.Text; +using CommunityToolkit.Diagnostics; using Microsoft.Extensions.DependencyInjection; using Shared.Core.ErrorHandling; using Shared.Core.ToolCreation; @@ -14,8 +15,8 @@ public interface IScopeRepository IEditorInterface CreateScope(Type owner); void RemoveScope(IEditorInterface owner); - T GetRequiredServiceRootScope(); - T GetRequiredService(IEditorInterface editorHandle); + T GetRequiredServiceRootScope() where T : notnull; + T GetRequiredService(IEditorInterface editorHandle) where T : notnull; void Print(); IEditorInterface? GetEditorFromToken(ScopeToken token); @@ -131,16 +132,22 @@ void GenerateDebugString(IServiceProvider provider, string scopeName, StringBuil // Get resolved services var serviceProviderType = provider.GetType(); var resolvedServicesProperty = serviceProviderType.GetProperty("ResolvedServices", BindingFlags.NonPublic | BindingFlags.Instance); + + Guard.IsNotNull(resolvedServicesProperty, "Failed get property ResolvedServices"); var resolved = resolvedServicesProperty.GetValue(provider); + Guard.IsNotNull(resolved, "Failed to get value from provider"); var resolveList = CastToObjectDictionary(resolved); + stringBuilder.AppendLine($"\tResolvedServices[{resolveList.Count}]"); foreach (var service in resolveList) stringBuilder.AppendLine($"\t\t{ service.Value.GetType().FullName}"); var disposablesServicesProperty = serviceProviderType.GetProperty("Disposables", BindingFlags.NonPublic | BindingFlags.Instance); + Guard.IsNotNull(disposablesServicesProperty, "Unable to find Disposables property on ServiceProvider"); var disposables = disposablesServicesProperty.GetValue(provider) as IList; - + Guard.IsNotNull(disposables, "Unable to cast Disposables to IList"); + stringBuilder.AppendLine($"\tDisposables[{disposables.Count}]"); foreach (var service in disposables) stringBuilder.AppendLine($"\t\t{service.GetType().FullName}"); diff --git a/Shared/SharedCore/Shared.Core/DevConfig/DevelopmentConfigurationManager.cs b/Shared/SharedCore/Shared.Core/DevConfig/DevelopmentConfigurationManager.cs index 4879fc855..f8593a37c 100644 --- a/Shared/SharedCore/Shared.Core/DevConfig/DevelopmentConfigurationManager.cs +++ b/Shared/SharedCore/Shared.Core/DevConfig/DevelopmentConfigurationManager.cs @@ -13,7 +13,7 @@ public class DevelopmentConfigurationManager private readonly IPackFileService _packFileService; private readonly ApplicationSettingsService _settingsService; private readonly IEnumerable _developerConfigurations; - private IDeveloperConfiguration _activeConfig; + private IDeveloperConfiguration? _activeConfig; public DevelopmentConfigurationManager(IEnumerable developerConfigurations, IPackFileService packFileService, ApplicationSettingsService settingsService) { diff --git a/Shared/SharedCore/Shared.Core/ErrorHandling/ApplicationStateRecorder.cs b/Shared/SharedCore/Shared.Core/ErrorHandling/ApplicationStateRecorder.cs index dfb35a19a..92352210c 100644 --- a/Shared/SharedCore/Shared.Core/ErrorHandling/ApplicationStateRecorder.cs +++ b/Shared/SharedCore/Shared.Core/ErrorHandling/ApplicationStateRecorder.cs @@ -4,7 +4,7 @@ namespace Shared.Core.ErrorHandling { public static class ApplicationStateRecorder { - static Stopwatch s_applicationTimer; + static Stopwatch? s_applicationTimer; static uint s_numberOfEditorsOpened = 0; public static void Initialize() @@ -18,7 +18,7 @@ public static void EditorOpened() s_numberOfEditorsOpened++; } - public static double GetApplicationRunTimeInSec() => s_applicationTimer.Elapsed.TotalSeconds; + public static double GetApplicationRunTimeInSec() => s_applicationTimer?.Elapsed.TotalSeconds ?? 0; public static uint GetNumberOfOpenedEditors() => s_numberOfEditorsOpened; } } diff --git a/Shared/SharedCore/Shared.Core/ErrorHandling/ErrorList.cs b/Shared/SharedCore/Shared.Core/ErrorHandling/ErrorList.cs index 31499954b..6f1d76a36 100644 --- a/Shared/SharedCore/Shared.Core/ErrorHandling/ErrorList.cs +++ b/Shared/SharedCore/Shared.Core/ErrorHandling/ErrorList.cs @@ -5,9 +5,9 @@ namespace Shared.Core.ErrorHandling [DebuggerDisplay("{ErrorType}:{ItemName}-{Description}")] public class ErrorListDataItem { - public string ErrorType { get; set; } - public string ItemName { get; set; } - public string Description { get; set; } + public string ErrorType { get; set; } = ""; + public string ItemName { get; set; } = ""; + public string Description { get; set; } = ""; public bool IsError { get; set; } = false; public bool IsWarning { get; set; } = false; diff --git a/Shared/SharedCore/Shared.Core/Events/EventHub.cs b/Shared/SharedCore/Shared.Core/Events/EventHub.cs index 3fb54fd7f..7b9990a45 100644 --- a/Shared/SharedCore/Shared.Core/Events/EventHub.cs +++ b/Shared/SharedCore/Shared.Core/Events/EventHub.cs @@ -40,7 +40,7 @@ abstract class EventHub : IDisposable private readonly ILogger _logger = Logging.Create(); bool _isDisposed = false; - Dictionary> _callbackList = new(); + readonly Dictionary> _callbackList = new(); private readonly IScopeRepository _scopeRepository; private readonly string _hubName; private readonly bool _isGlobal; @@ -135,6 +135,9 @@ public void Register(object owner, Action action) public void Dispose() { + if (_isDisposed == true) + return; + _isDisposed = true; _callbackList?.Clear(); } diff --git a/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs b/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs index 1f08fdbeb..3749b334f 100644 --- a/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs +++ b/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs @@ -49,6 +49,7 @@ public IEditorInterface Execute(EditorEnums editorEnum) public void ExecuteAsWindow(string fileName, int width, int heigh) { var file = _packFileService.FindFile(fileName); + Guard.IsNotNull(file, "File not found: " + fileName); var window = _editorCreator.CreateWindow(file); window.Width = width; diff --git a/Shared/SharedCore/Shared.Core/Misc/DirectoryHelper.cs b/Shared/SharedCore/Shared.Core/Misc/DirectoryHelper.cs index 8c71f84e9..4310d3884 100644 --- a/Shared/SharedCore/Shared.Core/Misc/DirectoryHelper.cs +++ b/Shared/SharedCore/Shared.Core/Misc/DirectoryHelper.cs @@ -6,8 +6,6 @@ namespace Shared.Core.Misc { public class DirectoryHelper { - - private const string ExplorerWindowClass = "CabinetWClass"; private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); @@ -51,7 +49,7 @@ public static void EnsureCreated() EnsureCreated(CacheDirectory); } - public static void EnsureCreated(string? path) + public static void EnsureCreated(string path) { if (!Directory.Exists(path)) Directory.CreateDirectory(path); @@ -60,7 +58,7 @@ public static void EnsureCreated(string? path) public static void EnsureFileFolderCreated(string filePath) { var folder = Path.GetDirectoryName(filePath); - if (!Directory.Exists(folder)) + if (!Directory.Exists(folder) && string.IsNullOrWhiteSpace(folder) == false) Directory.CreateDirectory(folder); } @@ -68,9 +66,7 @@ public static bool IsFileLocked(string path) { try { - using (Stream stream = new FileStream(path, FileMode.Open)) - { - } + using Stream stream = new FileStream(path, FileMode.Open); } catch (IOException) { @@ -105,8 +101,7 @@ public static void OpenOrFocusFolder(string folderPath) return; } - IntPtr foundHandle = IntPtr.Zero; - + var foundHandle = IntPtr.Zero; EnumWindows((hWnd, lParam) => { if (!IsWindowVisible(hWnd)) @@ -122,8 +117,7 @@ public static void OpenOrFocusFolder(string folderPath) GetWindowText(hWnd, windowTitle, windowTitle.Capacity); // Explorer title usually ends with folder name - string folderName = Path.GetFileName(folderPath); - + var folderName = Path.GetFileName(folderPath); if (!string.IsNullOrEmpty(folderName) && windowTitle.ToString().Contains(folderName, StringComparison.OrdinalIgnoreCase)) { diff --git a/Shared/SharedCore/Shared.Core/Misc/FileWatcher.cs b/Shared/SharedCore/Shared.Core/Misc/FileWatcher.cs index fbae3db5d..bec2e4788 100644 --- a/Shared/SharedCore/Shared.Core/Misc/FileWatcher.cs +++ b/Shared/SharedCore/Shared.Core/Misc/FileWatcher.cs @@ -1,4 +1,4 @@ -namespace Shared.Core.Misc +/*namespace Shared.Core.Misc { public class FileWatcher { @@ -61,4 +61,4 @@ public string Path set => _path = value; } } -} +}*/ diff --git a/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs b/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs index a7567aed0..5b6e4b97c 100644 --- a/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs +++ b/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs @@ -10,8 +10,7 @@ public override bool CanConvert(Type objecType) return objecType == typeof(List); } - public override object ReadJson(JsonReader reader, Type objecType, object existingValue, - JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { var token = JToken.Load(reader); if (token.Type == JTokenType.Array) @@ -26,9 +25,6 @@ public override bool CanWrite get { return false; } } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException(); } } diff --git a/Shared/SharedCore/Shared.Core/Misc/MathUtil.cs b/Shared/SharedCore/Shared.Core/Misc/MathUtil.cs index 413dedcf3..9700b4459 100644 --- a/Shared/SharedCore/Shared.Core/Misc/MathUtil.cs +++ b/Shared/SharedCore/Shared.Core/Misc/MathUtil.cs @@ -141,7 +141,7 @@ public static Matrix CreateRotation(Vector3 angles_degrees) return Matrix.CreateRotationX(x) * Matrix.CreateRotationY(y) * Matrix.CreateRotationZ(z); } - public static Matrix CreateRotation(Vector3[] vectors, Vector3[] basis = null) + public static Matrix CreateRotation(Vector3[] vectors, Vector3[]? basis = null) { if (vectors.Length != 3) { diff --git a/Shared/SharedCore/Shared.Core/Settings/ThemesController.cs b/Shared/SharedCore/Shared.Core/Settings/ThemesController.cs index fb31defba..09610d628 100644 --- a/Shared/SharedCore/Shared.Core/Settings/ThemesController.cs +++ b/Shared/SharedCore/Shared.Core/Settings/ThemesController.cs @@ -59,7 +59,8 @@ public static string GetEnumAsString(ThemeType theme) return theme switch { ThemeType.DarkTheme => "Dark", - ThemeType.LightTheme => "Light" + ThemeType.LightTheme => "Light", + _ => "Dark" }; } } From 928b63d9271623617f35a8753e28b2ac3056f508 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 19:25:43 +0200 Subject: [PATCH 22/26] Moving test files --- .../PackFileBrowserViewModelTestHelper.cs | 4 +-- .../PackFileBrowserViewModelTests.cs | 20 ++++++------- .../ClosePackContainerFileCommandTests.cs | 2 +- .../Commands/CollapseNodeCommandTests.cs | 2 +- .../Commands/ContextMenuCommandTestBase.cs | 2 +- .../Commands/CopyNodePathCommandTests.cs | 2 +- .../CopyToEditablePackCommandTests.cs | 2 +- .../Commands/CreateFolderCommandTests.cs | 2 +- .../Commands/DeleteNodeCommandTests.cs | 2 +- .../Commands/DuplicateFileCommandTests.cs | 23 +++++++++++++- .../Commands/ExpandNodeCommandTests.cs | 2 +- .../Commands/ExportToDirectoryCommandTests.cs | 2 +- .../Commands/ImportDirectoryCommandTests.cs | 2 +- .../Commands/ImportFileCommandTests.cs | 2 +- .../Commands/OpenNodeInHxDCommandTests.cs | 2 +- .../Commands/OpenNodeInNotepadCommandTests.cs | 2 +- .../OpenPackInFileExplorerCommandTests.cs | 2 +- .../Commands/RenameNodeCommandTests.cs | 2 +- .../SaveAsPackFileContainerCommandTests.cs | 2 +- .../SavePackFileContainerCommandTests.cs | 2 +- .../Commands/SetAsEditablePackCommandTests.cs | 2 +- .../{ => BaseDialogs}/TreeNodeTests.cs | 4 +-- .../Shared.UiTest/Shared.UiTest.csproj | 4 --- .../UiCommand_DuplicateFile_Tests.cs | 30 ------------------- 24 files changed, 54 insertions(+), 67 deletions(-) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileBrowserViewModelTestHelper.cs (94%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileBrowserViewModelTests.cs (97%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs (96%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs (97%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs (89%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs (96%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs (97%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs (96%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs (96%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs (71%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs (97%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs (98%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs (98%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs (98%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs (97%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs (97%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs (97%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs (96%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs (96%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs (96%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs (95%) rename Shared/SharedUI/Shared.UiTest/{ => BaseDialogs}/TreeNodeTests.cs (87%) delete mode 100644 Shared/SharedUI/Shared.UiTest/UiCommand_DuplicateFile_Tests.cs diff --git a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTestHelper.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileBrowserViewModelTestHelper.cs similarity index 94% rename from Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTestHelper.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileBrowserViewModelTestHelper.cs index 009572334..34f15beab 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTestHelper.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileBrowserViewModelTestHelper.cs @@ -1,6 +1,6 @@ -using Shared.Ui.BaseDialogs.PackFileTree; +using Shared.Ui.BaseDialogs.PackFileTree; -namespace Shared.UiTest +namespace Shared.UiTest.BaseDialogs { /// /// Test utility helper for PackFileBrowserViewModel testing. diff --git a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileBrowserViewModelTests.cs similarity index 97% rename from Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileBrowserViewModelTests.cs index 3e2697a3b..8749a00a8 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileBrowserViewModelTests.cs @@ -1,4 +1,4 @@ -// PackFileBrowserViewModel Lazy Loading Architecture: +// PackFileBrowserViewModel Lazy Loading Architecture: // // The tree is built lazily. When a container is added (via PackFileContainerAddedEvent), only // the root TreeNode is created with a child-loader delegate. Children are NOT loaded from @@ -35,7 +35,7 @@ using Test.TestingUtility.Shared; using Test.TestingUtility.TestUtility; -namespace Shared.UiTest +namespace Shared.UiTest.BaseDialogs { internal class PackFileBrowserViewModelTests { @@ -193,7 +193,7 @@ public void ClearingFilterKeepsExpandedFoldersAndShowsAllFiles() Assert.That(folderA, Is.Not.Null); folderA.IsNodeExpanded = true; - // Search for a specific file — folderA/sub gets expanded by filter, folderB hidden + // Search for a specific file — folderA/sub gets expanded by filter, folderB hidden _viewModel.Filter.FilterText = "match_file"; var sub = PackFileBrowserViewModelTestHelper.GetFromPath(root, "foldera\\sub"); @@ -472,10 +472,10 @@ public void ContainerSaved_ClearsUnsavedFlags() Assert.That(folderA, Is.Not.Null); Assert.That(root.UnsavedChanged, Is.True, "Root should be unsaved after adding file"); - // Simulate save via PFS — we can't call SavePackContainer without disk I/O, + // Simulate save via PFS — we can't call SavePackContainer without disk I/O, // so trigger the event directly through the event hub var eventHub = _runner.ServiceProvider.GetRequiredService(); - eventHub.PublishGlobalEvent(new Shared.Core.Events.Global.PackFileContainerSavedEvent(container)); + eventHub.PublishGlobalEvent(new Core.Events.Global.PackFileContainerSavedEvent(container)); Assert.That(root.UnsavedChanged, Is.False, "Root should be cleared after save"); } @@ -487,11 +487,11 @@ public void ContainerSaved_DoesNotForcePopulationOfUnloadedBranches() var root = _viewModel.Files[0]; var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); - // Root is collapsed — only root exists as materialized node + // Root is collapsed — only root exists as materialized node Assert.That(root.IsNodeExpanded, Is.False); var eventHub = _runner.ServiceProvider.GetRequiredService(); - eventHub.PublishGlobalEvent(new Shared.Core.Events.Global.PackFileContainerSavedEvent(container)); + eventHub.PublishGlobalEvent(new Core.Events.Global.PackFileContainerSavedEvent(container)); // After save, the tree should NOT have expanded/materialized children Assert.That(root.Children.All(c => c.Name == "" || c.NodeType == NodeType.Root), @@ -649,7 +649,7 @@ public void SearchAutoExpandsWhenResultCountBelowThreshold() var root = _viewModel.Files[0]; Assert.That(root.IsNodeExpanded, Is.False, "Root should start collapsed"); - // Filter matches all 3 files — well below the 25 threshold + // Filter matches all 3 files — well below the 25 threshold _viewModel.Filter.FilterText = "file"; // All matching nodes should be auto-expanded @@ -697,7 +697,7 @@ public void SearchDoesNotAutoExpandWhenResultCountAboveThreshold() public void DoubleClickCollapseAndReexpand_ChildrenRemainVisible() { // Scenario: user expands folders manually, collapses a parent, - // then re-expands it — child folders that were expanded should still show their children. + // then re-expands it — child folders that were expanded should still show their children. CreatePackfiles( ("folderA\\sub\\file1.txt", "file1.txt"), ("folderA\\sub\\file2.txt", "file2.txt")); @@ -747,7 +747,7 @@ public void OverwriteFileInCollapsedFolder_NoDuplicateNodeInTree() var root = _viewModel.Files[0]; var container = _packageFileService.GetAllPackfileContainers().Last(x => x.Name == "test.pack"); - // Folder is collapsed — BackingChildren are NOT yet loaded. + // Folder is collapsed — BackingChildren are NOT yet loaded. Assert.That(root.IsNodeExpanded, Is.False); // Act: add a second file with the same name (simulates Save As / overwrite import). diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs similarity index 96% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs index ffb9b16ac..75dd44d3d 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ClosePackContainerFileCommandTests.cs @@ -5,7 +5,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class ClosePackContainerFileCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs similarity index 97% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs index 05be0882c..144faf6cc 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CollapseNodeCommandTests.cs @@ -2,7 +2,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class CollapseNodeCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs similarity index 89% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs index e82188015..5cf4cf9c8 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ContextMenuCommandTestBase.cs @@ -1,7 +1,7 @@ using Moq; using Shared.Core.PackFiles.Models; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { internal abstract class ContextMenuCommandTestBase { diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs similarity index 96% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs index 0773b5d5c..536e103f0 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyNodePathCommandTests.cs @@ -5,7 +5,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class CopyNodePathCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs similarity index 97% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs index fcf17e44b..b59e16c8e 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CopyToEditablePackCommandTests.cs @@ -6,7 +6,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class CopyToEditablePackCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs similarity index 96% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs index 4f14966d1..258b13df7 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/CreateFolderCommandTests.cs @@ -6,7 +6,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class CreateFolderCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs similarity index 96% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs index 43be46754..5aec83984 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DeleteNodeCommandTests.cs @@ -6,7 +6,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class DeleteNodeCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs similarity index 71% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs index 0d30ca64f..f739f1a62 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs @@ -5,7 +5,7 @@ using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; using Test.TestingUtility.Shared; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class DuplicateFileCommandTests : ContextMenuCommandTestBase @@ -60,5 +60,26 @@ public void Execute_DuplicatesFileIntoEditablePack() var foundFile = runner.PackFileService.FindFile("Animation\\Meta\\testFile_copy.anm", outputPackFile); Assert.That(foundFile, Is.Not.Null); } + + [Test] + [TestCase("testFile", "testFile_copy")] // No extension + [TestCase("testFile.anm", "testFile_copy.anm")] // Single extension + [TestCase("testFile.anm.meta", "testFile_copy.anm.meta")] // Double extension + public void DuplicateFileCommand(string fileName, string result) + { + var runner = new AssetEditorTestRunner(); + runner.PackFileService.EnforceGameFilesMustBeLoaded = false; + var sourcePackFile = runner.CreateEmptyPackFile("SourcePack", false); + var outputPackFile = runner.CreateEmptyPackFile("OutputPack", true); + + var fileEntry = new NewPackFileEntry("Animation\\Meta", PackFile.CreateFromASCII(fileName, "DummyContent")); + runner.PackFileService.AddFilesToPack(sourcePackFile, [fileEntry]); + var fileToCopy = runner.PackFileService.FindFile("Animation\\Meta\\" + fileName, sourcePackFile); + + runner.CommandFactory.Create().Execute(fileToCopy); + var foundFile = runner.PackFileService.FindFile("Animation\\Meta\\" + result, outputPackFile); + Assert.That(foundFile, Is.Not.Null); + } } } +} diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs similarity index 97% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs index c460cbe9b..8d1f693ea 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ExpandNodeCommandTests.cs @@ -3,7 +3,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class ExpandNodeCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs similarity index 98% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs index ac0b60906..334934297 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ExportToDirectoryCommandTests.cs @@ -7,7 +7,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class ExportToDirectoryCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs similarity index 98% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs index 4d0b2b084..e677f2ab1 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommandTests.cs @@ -8,7 +8,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class ImportDirectoryCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs similarity index 98% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs index 8986f91ea..bdb6d64a3 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportFileCommandTests.cs @@ -8,7 +8,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class ImportFileCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs similarity index 97% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs index 7f0d39f34..cd506d795 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInHxDCommandTests.cs @@ -5,7 +5,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class OpenNodeInHxDCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs similarity index 97% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs index d8c7cd34c..dc87e9f9b 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenNodeInNotepadCommandTests.cs @@ -5,7 +5,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class OpenNodeInNotepadCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs similarity index 97% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs index 1447e4aa1..9c209e58e 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/OpenPackInFileExplorerCommandTests.cs @@ -6,7 +6,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class OpenPackInFileExplorerCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs similarity index 96% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs index 4245a6eee..b87a7cfd5 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/RenameNodeCommandTests.cs @@ -6,7 +6,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class RenameNodeCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs similarity index 96% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs index c890bf06a..a7e0abcb0 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/SaveAsPackFileContainerCommandTests.cs @@ -7,7 +7,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class SaveAsPackFileContainerCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs similarity index 96% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs index fc6f257f9..35c48b6cb 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/SavePackFileContainerCommandTests.cs @@ -6,7 +6,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class SavePackFileContainerCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs similarity index 95% rename from Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs index bc2d543e4..af9b2a916 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/SetAsEditablePackCommandTests.cs @@ -5,7 +5,7 @@ using Shared.Ui.BaseDialogs.PackFileTree; using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -namespace Shared.UiTest.PackFileTree.ContextMenu.Commands +namespace Shared.UiTest.BaseDialogs.PackFileTree.ContextMenu.Commands { [TestFixture] internal class SetAsEditablePackCommandTests : ContextMenuCommandTestBase diff --git a/Shared/SharedUI/Shared.UiTest/TreeNodeTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/TreeNodeTests.cs similarity index 87% rename from Shared/SharedUI/Shared.UiTest/TreeNodeTests.cs rename to Shared/SharedUI/Shared.UiTest/BaseDialogs/TreeNodeTests.cs index c577da06d..b7ab988e7 100644 --- a/Shared/SharedUI/Shared.UiTest/TreeNodeTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/TreeNodeTests.cs @@ -1,7 +1,7 @@ -using Shared.Core.PackFiles.Models.Containers; +using Shared.Core.PackFiles.Models.Containers; using Shared.Ui.BaseDialogs.PackFileTree; -namespace Shared.UiTest +namespace Shared.UiTest.BaseDialogs { internal class TreeNodeTests { diff --git a/Shared/SharedUI/Shared.UiTest/Shared.UiTest.csproj b/Shared/SharedUI/Shared.UiTest/Shared.UiTest.csproj index 07cd68640..9bed98bbe 100644 --- a/Shared/SharedUI/Shared.UiTest/Shared.UiTest.csproj +++ b/Shared/SharedUI/Shared.UiTest/Shared.UiTest.csproj @@ -32,8 +32,4 @@ - - - - diff --git a/Shared/SharedUI/Shared.UiTest/UiCommand_DuplicateFile_Tests.cs b/Shared/SharedUI/Shared.UiTest/UiCommand_DuplicateFile_Tests.cs deleted file mode 100644 index 37fbf47a4..000000000 --- a/Shared/SharedUI/Shared.UiTest/UiCommand_DuplicateFile_Tests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Shared.Core.PackFiles; -using Shared.Core.PackFiles.Models; -using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands; -using Test.TestingUtility.Shared; - -namespace Shared.UiTest -{ - internal class UiCommand_DuplicateFile_Tests - { - [Test] - [TestCase("testFile", "testFile_copy")] // No extension - [TestCase("testFile.anm", "testFile_copy.anm")] // Single extension - [TestCase("testFile.anm.meta", "testFile_copy.anm.meta")] // Double extension - public void DuplicateFileCommand(string fileName, string result) - { - var runner = new AssetEditorTestRunner(); - runner.PackFileService.EnforceGameFilesMustBeLoaded = false; - var sourcePackFile = runner.CreateEmptyPackFile("SourcePack", false); - var outputPackFile = runner.CreateEmptyPackFile("OutputPack", true); - - var fileEntry = new NewPackFileEntry("Animation\\Meta", PackFile.CreateFromASCII(fileName, "DummyContent")); - runner.PackFileService.AddFilesToPack(sourcePackFile, [fileEntry]); - var fileToCopy = runner.PackFileService.FindFile("Animation\\Meta\\" + fileName, sourcePackFile); - - runner.CommandFactory.Create().Execute(fileToCopy); - var foundFile = runner.PackFileService.FindFile("Animation\\Meta\\" + result, outputPackFile); - Assert.That(foundFile, Is.Not.Null); - } - } -} From ca7d56624df4d88e161c74c9b54ab2788b48400c Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 20:44:42 +0200 Subject: [PATCH 23/26] Fix warnings --- .../AdvancedRmvMaterialView.xaml | 12 +++---- .../Mesh.Material/Emissive/EmissiveView.xaml | 30 ++++++++-------- .../MeshNode/Mesh.Material/Tint/TintView.xaml | 8 ++--- .../Shared.Core/Misc/JsonHelpers.cs | 3 +- .../Shared.Ui/BaseDialogs/AeAttribute.xaml.cs | 36 ++----------------- .../PackFileTree/PackFileBrowserViewModel.cs | 4 +-- .../Shared.Ui/Common/AssetEditorWindow.cs | 4 ++- .../Shared.Ui/Common/DataGridNumericColumn.cs | 2 ++ .../SharedUI/Shared.Ui/Common/IDropTarget.cs | 14 +++----- .../Common/MenuSystem/ActionHotkeyHandler.cs | 9 ++--- .../Shared.Ui/Common/MenuSystem/Hotkey.cs | 6 +--- .../Common/MenuSystem/MenuBarButton.cs | 10 ++---- .../Common/MenuSystem/ToolbarItem.cs | 8 ++--- .../Common/ToolTipSystem/ToolTipEnum.cs | 23 ------------ .../Common/ToolTipSystem/ToolTipManager.cs | 29 --------------- .../Commands/DuplicateFileCommandTests.cs | 2 +- 16 files changed, 50 insertions(+), 150 deletions(-) delete mode 100644 Shared/SharedUI/Shared.Ui/Common/ToolTipSystem/ToolTipEnum.cs delete mode 100644 Shared/SharedUI/Shared.Ui/Common/ToolTipSystem/ToolTipManager.cs diff --git a/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/AdvancedRmvMaterial/AdvancedRmvMaterialView.xaml b/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/AdvancedRmvMaterial/AdvancedRmvMaterialView.xaml index 37b25b24c..76a7717a4 100644 --- a/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/AdvancedRmvMaterial/AdvancedRmvMaterialView.xaml +++ b/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/AdvancedRmvMaterial/AdvancedRmvMaterialView.xaml @@ -41,27 +41,27 @@ - + - + - + - + - + - + diff --git a/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/Emissive/EmissiveView.xaml b/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/Emissive/EmissiveView.xaml index 559571afb..f8e17a2f7 100644 --- a/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/Emissive/EmissiveView.xaml +++ b/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/Emissive/EmissiveView.xaml @@ -28,31 +28,31 @@ - + - + - + - + - + - + - + - + @@ -70,7 +70,7 @@ - + - + - + - + - + - + - + diff --git a/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/Tint/TintView.xaml b/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/Tint/TintView.xaml index 1be4d67d5..3018c8b7f 100644 --- a/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/Tint/TintView.xaml +++ b/Editors/Kitbashing/KitbasherEditor/Core/SceneNodeEditor/Nodes/MeshNode/Mesh.Material/Tint/TintView.xaml @@ -26,17 +26,17 @@ - + - + - + @@ -53,7 +53,7 @@ - + diff --git a/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs b/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs index 5b6e4b97c..fa19347b0 100644 --- a/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs +++ b/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs @@ -17,7 +17,8 @@ public override bool CanConvert(Type objecType) { return token.ToObject>(); } - return new List { token.ToObject() }; + var tokenT = token.ToObject(); + return new List { tokenT }; } public override bool CanWrite diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/AeAttribute.xaml.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/AeAttribute.xaml.cs index a1204b7fc..ea513030b 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/AeAttribute.xaml.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/AeAttribute.xaml.cs @@ -3,7 +3,7 @@ using System.Windows.Controls; using System.Windows.Markup; using Shared.EmbeddedResources; -using Shared.Ui.Common.ToolTipSystem; + namespace Shared.Ui.BaseDialogs { @@ -14,36 +14,11 @@ public partial class AeAttribute : UserControl public AeAttribute() { InitializeComponent(); - + InformationImage.Source = IconLibrary.InformationIcon; InformationImage.Visibility = Visibility.Hidden; } - private static void OnToolTipEnumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var owner = (AeAttribute)d; - var value = (ToolTipEnum)e.NewValue; - if (value == ToolTipEnum.None) - owner.InformationImage.Visibility = Visibility.Hidden; - else - owner.InformationImage.Visibility = Visibility.Visible; - - if (value == ToolTipEnum.None) - { - owner.InformationImage.Visibility = Visibility.Collapsed; - owner.InformationImage.ToolTip = null; - owner.Text.ToolTip = null; - return; - } - - var text = ToolTips.List[value]; - var toolTip = new ToolTip() { Content = text }; - - owner.InformationImage.Visibility = Visibility.Visible; - owner.InformationImage.ToolTip = toolTip; - owner.Text.ToolTip = toolTip; - } - public static readonly DependencyProperty InnerContentProperty = DependencyProperty.Register("InnerContent", typeof(object), typeof(AeAttribute)); public object InnerContent { @@ -51,13 +26,6 @@ public object InnerContent set { SetValue(InnerContentProperty, value); } } - public static readonly DependencyProperty ToolTipEnumValueProperty = DependencyProperty.Register(nameof(ToolTipEnumValue), typeof(ToolTipEnum), typeof(AeAttribute), new PropertyMetadata(ToolTipEnum.None, OnToolTipEnumChanged)); - public ToolTipEnum ToolTipEnumValue - { - get { return (ToolTipEnum)GetValue(ToolTipEnumValueProperty); } - set { SetValue(ToolTipEnumValueProperty, value);} - } - public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register(nameof(LabelText), typeof(string), typeof(AeAttribute), new PropertyMetadata(null)); public string LabelText { diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs index 02d5354e0..81db110fa 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs @@ -385,7 +385,7 @@ private void OnPackFileContainerRemoved(PackFileContainerRemovedEvent e) } } - public bool AllowDrop(TreeNode node, TreeNode targetNode = null) + public bool AllowDrop(TreeNode node, TreeNode? targetNode = null) { if (node.Item == null) return false; @@ -402,7 +402,7 @@ public bool AllowDrop(TreeNode node, TreeNode targetNode = null) return true; } - public bool Drop(TreeNode node, TreeNode targeNode) + public bool Drop(TreeNode node, TreeNode? targeNode) { var container = node.FileOwner; var draggedFile = node.Item; diff --git a/Shared/SharedUI/Shared.Ui/Common/AssetEditorWindow.cs b/Shared/SharedUI/Shared.Ui/Common/AssetEditorWindow.cs index de4f06de8..bc200c6d6 100644 --- a/Shared/SharedUI/Shared.Ui/Common/AssetEditorWindow.cs +++ b/Shared/SharedUI/Shared.Ui/Common/AssetEditorWindow.cs @@ -1,5 +1,6 @@ using System; using System.Windows; +using CommunityToolkit.Diagnostics; // there is a bug in visual studio code generation. Having generated classes with // Overlapping names (Shared.Ui and Editors.Shared) causes compile errors as the code @@ -28,9 +29,10 @@ public AssetEditorWindow() private void AssetEdWindow_Deactivated(object? sender, EventArgs e) { - if (AlwaysOnTop) + if (AlwaysOnTop && sender != null) { var window = (Window)sender; + Guard.IsNotNull(window, "window should not be null"); window.Topmost = true; } } diff --git a/Shared/SharedUI/Shared.Ui/Common/DataGridNumericColumn.cs b/Shared/SharedUI/Shared.Ui/Common/DataGridNumericColumn.cs index 7640b127e..dd47cbff9 100644 --- a/Shared/SharedUI/Shared.Ui/Common/DataGridNumericColumn.cs +++ b/Shared/SharedUI/Shared.Ui/Common/DataGridNumericColumn.cs @@ -1,5 +1,6 @@ using System; using System.Windows.Controls; +using CommunityToolkit.Diagnostics; namespace Shared.Ui.Common { @@ -8,6 +9,7 @@ public class DataGridNumericColumn : DataGridTextColumn protected override object PrepareCellForEdit(System.Windows.FrameworkElement editingElement, System.Windows.RoutedEventArgs editingEventArgs) { var edit = editingElement as TextBox; + Guard.IsNotNull(edit, "Casting to textbox returned null"); edit.PreviewTextInput += OnPreviewTextInput; return base.PrepareCellForEdit(editingElement, editingEventArgs); diff --git a/Shared/SharedUI/Shared.Ui/Common/IDropTarget.cs b/Shared/SharedUI/Shared.Ui/Common/IDropTarget.cs index 2f853ff9c..fa3bb95e2 100644 --- a/Shared/SharedUI/Shared.Ui/Common/IDropTarget.cs +++ b/Shared/SharedUI/Shared.Ui/Common/IDropTarget.cs @@ -1,18 +1,14 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Shared.Ui.Common +namespace Shared.Ui.Common { public interface IDropTarget { - bool AllowDrop(T node, T targeNode = default); - bool Drop(T node, T targeNode = default); + bool AllowDrop(T node, T? targeNode = default); + bool Drop(T node, T? targeNode = default); } public interface IDropTarget { - bool AllowDrop(T node, T targeNode = default, U additionalData = default); - bool Drop(T node, T targeNode = default, U additionalData = default); + bool AllowDrop(T node, T? targeNode = default, U? additionalData = default); + bool Drop(T node, T? targeNode = default, U? additionalData = default); } } diff --git a/Shared/SharedUI/Shared.Ui/Common/MenuSystem/ActionHotkeyHandler.cs b/Shared/SharedUI/Shared.Ui/Common/MenuSystem/ActionHotkeyHandler.cs index d181b86b3..cb1efbd6c 100644 --- a/Shared/SharedUI/Shared.Ui/Common/MenuSystem/ActionHotkeyHandler.cs +++ b/Shared/SharedUI/Shared.Ui/Common/MenuSystem/ActionHotkeyHandler.cs @@ -1,15 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; +using System.Collections.Generic; using System.Windows.Input; namespace Shared.Ui.Common.MenuSystem { public class ActionHotkeyHandler { - List _actions = new List(); + readonly List _actions = new(); public void Register(MenuAction action) { @@ -34,6 +30,5 @@ public bool TriggerCommand(Key key, ModifierKeys modifierKeys) return isHandled; } - } } diff --git a/Shared/SharedUI/Shared.Ui/Common/MenuSystem/Hotkey.cs b/Shared/SharedUI/Shared.Ui/Common/MenuSystem/Hotkey.cs index 3a56ce003..a1eda74fe 100644 --- a/Shared/SharedUI/Shared.Ui/Common/MenuSystem/Hotkey.cs +++ b/Shared/SharedUI/Shared.Ui/Common/MenuSystem/Hotkey.cs @@ -1,8 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Windows.Input; +using System.Windows.Input; namespace Shared.Ui.Common.MenuSystem { diff --git a/Shared/SharedUI/Shared.Ui/Common/MenuSystem/MenuBarButton.cs b/Shared/SharedUI/Shared.Ui/Common/MenuSystem/MenuBarButton.cs index fb2e2eb6b..221dee5e4 100644 --- a/Shared/SharedUI/Shared.Ui/Common/MenuSystem/MenuBarButton.cs +++ b/Shared/SharedUI/Shared.Ui/Common/MenuSystem/MenuBarButton.cs @@ -1,8 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Windows.Media.Imaging; +using System.Windows.Media.Imaging; using Shared.Core.Misc; namespace Shared.Ui.Common.MenuSystem @@ -11,8 +7,8 @@ namespace Shared.Ui.Common.MenuSystem public class MenuBarButton { public NotifyAttr IsVisible { get; set; } = new NotifyAttr(true); - public BitmapImage Image { get; set; } - public MenuAction Action { get; set; } + public BitmapImage? Image { get; set; } + public MenuAction? Action { get; set; } public ButtonVisibilityRule ShowRule { get; set; } = ButtonVisibilityRule.Always; public bool IsSeperator { get; set; } = false; diff --git a/Shared/SharedUI/Shared.Ui/Common/MenuSystem/ToolbarItem.cs b/Shared/SharedUI/Shared.Ui/Common/MenuSystem/ToolbarItem.cs index 99f6afced..65a14554a 100644 --- a/Shared/SharedUI/Shared.Ui/Common/MenuSystem/ToolbarItem.cs +++ b/Shared/SharedUI/Shared.Ui/Common/MenuSystem/ToolbarItem.cs @@ -1,8 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using Shared.Core.Misc; namespace Shared.Ui.Common.MenuSystem @@ -12,7 +8,7 @@ public class ToolbarItem public NotifyAttr NameAttribute { get; set; } = new NotifyAttr(""); public ObservableCollection Children { get; set; } = new ObservableCollection(); - string _name; + string _name = ""; public string Name { get => _name; diff --git a/Shared/SharedUI/Shared.Ui/Common/ToolTipSystem/ToolTipEnum.cs b/Shared/SharedUI/Shared.Ui/Common/ToolTipSystem/ToolTipEnum.cs deleted file mode 100644 index e4d2e92ca..000000000 --- a/Shared/SharedUI/Shared.Ui/Common/ToolTipSystem/ToolTipEnum.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace Shared.Ui.Common.ToolTipSystem -{ - public enum ToolTipEnum - { - None, - - Kitbash_WsMaterial_Emissive_Strength - } - - public static class ToolTips - { - private readonly static Dictionary s_toolTips = new() - { - {ToolTipEnum.None, ""}, - - }; - - public static Dictionary List { get => s_toolTips; } - } - -} diff --git a/Shared/SharedUI/Shared.Ui/Common/ToolTipSystem/ToolTipManager.cs b/Shared/SharedUI/Shared.Ui/Common/ToolTipSystem/ToolTipManager.cs deleted file mode 100644 index e6078ae92..000000000 --- a/Shared/SharedUI/Shared.Ui/Common/ToolTipSystem/ToolTipManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Shared.Ui.Common.ToolTipSystem -{ - public static class ToolTipManager - { - readonly static Dictionary _toolTips = new Dictionary { { ToolTipEnum.None, "" } }; - - static void RegisterToolTip(ToolTipEnum enumValue, string toolTip) - { - if (_toolTips.ContainsKey(enumValue)) - throw new Exception($"{nameof(ToolTipManager)} alread have been assigned {enumValue}. Current value is '{_toolTips[enumValue]}'"); - _toolTips.Add(enumValue, toolTip); - } - static string GetToolTip(ToolTipEnum enumValue) => _toolTips[enumValue]; - - public static void Validate() - { - foreach (ToolTipEnum item in Enum.GetValues(typeof(ToolTipEnum))) - { - var result = _toolTips.TryGetValue(item, out var toolTip); - if (result == false) - throw new Exception($"ToolTipEnum {item} has not been assigned!"); - } - } - } - -} diff --git a/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs index f739f1a62..c461b343a 100644 --- a/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs +++ b/Shared/SharedUI/Shared.UiTest/BaseDialogs/PackFileTree/ContextMenu/Commands/DuplicateFileCommandTests.cs @@ -82,4 +82,4 @@ public void DuplicateFileCommand(string fileName, string result) } } } -} + From 4726f66b307ea1c8dda5dedea45a90bfbdf2b9c7 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 21:08:29 +0200 Subject: [PATCH 24/26] Fix warnings --- AssetEditor/ViewModels/SettingsViewModel.cs | 2 +- .../DependencyInjection/ScopeRepository.cs | 2 +- .../SharedCore/Shared.Core/ErrorHandling/Logging.cs | 2 +- .../SharedCore/Shared.Core/ErrorHandling/Result.cs | 7 ++----- Shared/SharedCore/Shared.Core/Events/EventHub.cs | 5 ++--- Shared/SharedCore/Shared.Core/Misc/NotifyAttr.cs | 4 +++- .../Shared.Core/Misc/NotifyPropertyChangedImpl.cs | 4 ++-- .../SharedCore/Shared.Core/Misc/ReflectionHelper.cs | 10 +++++----- Shared/SharedCore/Shared.Core/Misc/SaveUtility.cs | 6 +++--- .../Shared.Core/PackFiles/Models/PFHeader.cs | 8 +++++++- .../Serialization/PackFileSerializerLoader.cs | 13 ++++++------- .../Shared.Core/PackFiles/Utility/PackFileUtil.cs | 9 ++++++--- .../Settings/ApplicationSettingsService.cs | 9 ++------- .../Utility/PackFileContainerLoaderTests.cs | 5 ++--- 14 files changed, 43 insertions(+), 43 deletions(-) diff --git a/AssetEditor/ViewModels/SettingsViewModel.cs b/AssetEditor/ViewModels/SettingsViewModel.cs index c3b7a2120..850b442c7 100644 --- a/AssetEditor/ViewModels/SettingsViewModel.cs +++ b/AssetEditor/ViewModels/SettingsViewModel.cs @@ -94,7 +94,7 @@ private void Save() _settingsService.CurrentSettings.CameraControlMode = SelectedCameraMode; foreach (var item in GameDirectores) - _settingsService.CurrentSettings.GameDirectories.Add(new ApplicationSettings.GamePathPair() { Game = item.GameType, Path = item.Path }); + _settingsService.CurrentSettings.GameDirectories.Add(new ApplicationSettings.GamePathPair(item.GameType, item.Path)); _settingsService.CurrentSettings.WwisePath = WwisePath; _localizationManager.LoadLanguage(SelectedLanguage); diff --git a/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs b/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs index 7d9974f52..f1fa8f73d 100644 --- a/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs +++ b/Shared/SharedCore/Shared.Core/DependencyInjection/ScopeRepository.cs @@ -174,7 +174,7 @@ static Dictionary CastToObjectDictionary(object input) foreach (var key in dictionary.Keys) { var value = dictionary[key]; - objectDictionary[key] = value; + objectDictionary[key] = value!; } return objectDictionary; diff --git a/Shared/SharedCore/Shared.Core/ErrorHandling/Logging.cs b/Shared/SharedCore/Shared.Core/ErrorHandling/Logging.cs index df7d75ae5..095517ec0 100644 --- a/Shared/SharedCore/Shared.Core/ErrorHandling/Logging.cs +++ b/Shared/SharedCore/Shared.Core/ErrorHandling/Logging.cs @@ -25,7 +25,7 @@ public class Logging public static ILogger CreateStatic(Type type) => Log.ForContext(type); static bool IsConfigured = false; - public static string LogName { get; private set; } + public static string LogName { get; private set; } = string.Empty; public static CustomLoggingSink? CustomSink; public static void Configure(LogEventLevel logEventLevel) diff --git a/Shared/SharedCore/Shared.Core/ErrorHandling/Result.cs b/Shared/SharedCore/Shared.Core/ErrorHandling/Result.cs index b95c6b437..9d4d76473 100644 --- a/Shared/SharedCore/Shared.Core/ErrorHandling/Result.cs +++ b/Shared/SharedCore/Shared.Core/ErrorHandling/Result.cs @@ -4,17 +4,16 @@ namespace Shared.Core.ErrorHandling { public class Result { - public ErrorList LogItems { get; private set; } + public ErrorList LogItems { get; private set; } = new(); public bool IsSuccess { get; private set; } public bool Failed { get => !IsSuccess; } - public T Item { get; private set; } + public T? Item { get; private set; } public static Result FromError(string errorGroup, string description) { var item = new Result() { IsSuccess = false, - LogItems = new ErrorList() }; item.LogItems.Error(errorGroup, description); return item; @@ -25,7 +24,6 @@ public static Result FromError(string errorGroup, ValidationResult result) var item = new Result() { IsSuccess = false, - LogItems = new ErrorList() }; foreach (var error in result.Errors) item.LogItems.Error(errorGroup, $"{error.PropertyName} - {error.ErrorMessage}"); @@ -37,7 +35,6 @@ public static Result FromError(ErrorList errorList) var item = new Result() { IsSuccess = false, - LogItems = new ErrorList() }; foreach (var error in errorList.Errors) item.LogItems.Error(error.ErrorType, error.Description); diff --git a/Shared/SharedCore/Shared.Core/Events/EventHub.cs b/Shared/SharedCore/Shared.Core/Events/EventHub.cs index 7b9990a45..0d5e8f3de 100644 --- a/Shared/SharedCore/Shared.Core/Events/EventHub.cs +++ b/Shared/SharedCore/Shared.Core/Events/EventHub.cs @@ -1,5 +1,4 @@ -using Serilog; -using Shared.Core.DependencyInjection; +using Shared.Core.DependencyInjection; using Shared.Core.ErrorHandling; namespace Shared.Core.Events @@ -57,7 +56,7 @@ public void PublishGlobalEvent(T e) // If gloabl, send to self and all children if (_isGlobal) { - _logger.Here().Information($"Publshing global event {e.GetType().Name} on {_hubName}"); + _logger.Here().Information($"Publshing global event {e!.GetType().Name} on {_hubName}"); // Send to global subscribers Publish(e); diff --git a/Shared/SharedCore/Shared.Core/Misc/NotifyAttr.cs b/Shared/SharedCore/Shared.Core/Misc/NotifyAttr.cs index c3cd57a80..db66dcf8f 100644 --- a/Shared/SharedCore/Shared.Core/Misc/NotifyAttr.cs +++ b/Shared/SharedCore/Shared.Core/Misc/NotifyAttr.cs @@ -11,16 +11,18 @@ public class NotifyAttr : NotifyPropertyChangedImpl public NotifyAttr(T value) { + _value = value; Value = value; } public NotifyAttr() { - Value = default; + _value = default; } public NotifyAttr(T value, ValueChangedDelegate onValueChanged) { + _value = value; Value = value; _onValueChanged = onValueChanged; } diff --git a/Shared/SharedCore/Shared.Core/Misc/NotifyPropertyChangedImpl.cs b/Shared/SharedCore/Shared.Core/Misc/NotifyPropertyChangedImpl.cs index 77934f342..939837498 100644 --- a/Shared/SharedCore/Shared.Core/Misc/NotifyPropertyChangedImpl.cs +++ b/Shared/SharedCore/Shared.Core/Misc/NotifyPropertyChangedImpl.cs @@ -26,7 +26,7 @@ protected virtual void NotifyPropertyChanged(T value, ValueChangedDelegate valueChangedDelegate?.Invoke(value); } - protected virtual void SetAndNotify(ref T variable, T newValue, ValueChangedDelegate valueChangedDelegate = null, [CallerMemberName] string propertyName = "") + protected virtual void SetAndNotify(ref T variable, T newValue, ValueChangedDelegate? valueChangedDelegate = null, [CallerMemberName] string propertyName = "") { variable = newValue; NotifyPropertyChanged(propertyName); @@ -51,7 +51,7 @@ protected virtual void SetAndNotifyWithSender(T value, ValueAndSenderChangedD valueChangedDelegate?.Invoke(this, value); } - protected virtual void SetAndNotifyWithSender(ref T variable, T newValue, ValueAndSenderChangedDelegate valueChangedDelegate = null, [CallerMemberName] string propertyName = "") + protected virtual void SetAndNotifyWithSender(ref T variable, T newValue, ValueAndSenderChangedDelegate? valueChangedDelegate = null, [CallerMemberName] string propertyName = "") { variable = newValue; NotifyPropertyChanged(propertyName); diff --git a/Shared/SharedCore/Shared.Core/Misc/ReflectionHelper.cs b/Shared/SharedCore/Shared.Core/Misc/ReflectionHelper.cs index cbd9f4c9e..ae0e54930 100644 --- a/Shared/SharedCore/Shared.Core/Misc/ReflectionHelper.cs +++ b/Shared/SharedCore/Shared.Core/Misc/ReflectionHelper.cs @@ -15,17 +15,17 @@ public static object GetMemberValue(object obj, string memberName) // Try to get as a Property var propInfo = type.GetProperty(memberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (propInfo != null) - return propInfo.GetValue(obj, null); + return propInfo.GetValue(obj, null)!; // Try to get as a Field var fieldInfo = type.GetField(memberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (fieldInfo != null) - return fieldInfo.GetValue(obj); + return fieldInfo.GetValue(obj)!; throw new MemberAccessException($"Member '{memberName}' not found in type '{type.Name}'."); } - public static T CreateShallowCopy(T original) where T : class + public static T? CreateShallowCopy(T original) where T : class { if (original == null) { @@ -34,7 +34,7 @@ public static T CreateShallowCopy(T original) where T : class // Create a new instance of the same type as the original object. // Activator.CreateInstance() uses reflection to instantiate the type dynamically. - T copy = (T)Activator.CreateInstance(original.GetType()); + T copy = (T)Activator.CreateInstance(original.GetType())!; // Get all public, instance properties of the class. PropertyInfo[] properties = original.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); @@ -45,7 +45,7 @@ public static T CreateShallowCopy(T original) where T : class if (property.CanRead && property.CanWrite) { // Get the value from the original object and set it on the new copy. - object value = property.GetValue(original); + object value = property.GetValue(original)!; property.SetValue(copy, value); } } diff --git a/Shared/SharedCore/Shared.Core/Misc/SaveUtility.cs b/Shared/SharedCore/Shared.Core/Misc/SaveUtility.cs index c2056266d..2c42aa2a4 100644 --- a/Shared/SharedCore/Shared.Core/Misc/SaveUtility.cs +++ b/Shared/SharedCore/Shared.Core/Misc/SaveUtility.cs @@ -36,9 +36,9 @@ public static void CreateFileBackup(string originalFileName) var dirName = Path.GetDirectoryName(originalFileName); var fileName = Path.GetFileNameWithoutExtension(originalFileName); var extention = Path.GetExtension(originalFileName); - var uniqeFileName = IndexedFilename(Path.Combine(dirName, s_backupFolderPath, fileName), extention); + var uniqeFileName = IndexedFilename(Path.Combine(dirName!, s_backupFolderPath, fileName), extention); - Directory.CreateDirectory(Path.Combine(dirName, s_backupFolderPath)); + Directory.CreateDirectory(Path.Combine(dirName!, s_backupFolderPath)); File.Copy(originalFileName, uniqeFileName); } } @@ -46,7 +46,7 @@ public static void CreateFileBackup(string originalFileName) static string IndexedFilename(string stub, string extension) { var ix = 0; - string filename = null; + string? filename = null; do { ix++; diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/PFHeader.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/PFHeader.cs index bea1bd5e0..6575817d0 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/PFHeader.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/PFHeader.cs @@ -55,7 +55,13 @@ public class PFHeader public List DependantFiles { get; set; } = []; - public PFHeader() { } + public PFHeader(string strVersion, int type, uint referenceFileCount) + { + StrVersion = strVersion; + ByteMask = type; + ReferenceFileCount = referenceFileCount; + Buffer = DefaultTimeStamp; + } public PFHeader(string version, PackFileCAType type) { diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileSerializerLoader.cs b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileSerializerLoader.cs index fa835a4d8..dc8e7ed8d 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileSerializerLoader.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileSerializerLoader.cs @@ -10,7 +10,7 @@ namespace Shared.Core.PackFiles.Serialization { public static class PackFileVersionConverter { - static List<(PackFileVersion EnumValue, string StringValue)> s_values = new List<(PackFileVersion EnumValue, string StringValue)>() + readonly static List<(PackFileVersion EnumValue, string StringValue)> s_values = new List<(PackFileVersion EnumValue, string StringValue)>() { (PackFileVersion.PFH0, "PFH0"), (PackFileVersion.PFH2, "PFH2"), @@ -121,12 +121,11 @@ public static PackFileContainer Load(string packFileSystemPath, long packFileSiz static PFHeader ReadHeader(BinaryReader reader) { var fileNameBuffer = new byte[1024]; - var header = new PFHeader() - { - StrVersion = new string(reader.ReadChars(4)), // 4 - ByteMask = reader.ReadInt32(), // 8 - ReferenceFileCount = reader.ReadUInt32(), // 12 - }; + + var header_strVersion = new string(reader.ReadChars(4)); // 4 + var header_byteMask = reader.ReadInt32(); // 8 + var header_referenceFileCount = reader.ReadUInt32(); // 12 + var header = new PFHeader(header_strVersion, header_byteMask, header_referenceFileCount); var pack_file_index_size = reader.ReadUInt32(); // 16 var pack_file_count = reader.ReadUInt32(); // 20 diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileUtil.cs b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileUtil.cs index 6beb0d742..bc5d2192c 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileUtil.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileUtil.cs @@ -1,4 +1,5 @@ -using Shared.Core.PackFiles.Models; +using CommunityToolkit.Diagnostics; +using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Models.FileSources; namespace Shared.Core.PackFiles.Utility @@ -70,7 +71,9 @@ public static List LoadFilesFromDisk(IPackFileService pfs, IEnumerable packFileList.Add( new NewPackFileEntry(fileRef.PackFilePath, packfile)); } - pfs.AddFilesToPack(pfs.GetEditablePack(), packFileList); + var editablePack = pfs.GetEditablePack(); + Guard.IsNotNull(editablePack, "EditablePack returns null"); + pfs.AddFilesToPack(editablePack, packFileList); return packFileList.Select(x=>x.PackFile).ToList(); } @@ -80,7 +83,7 @@ public class FileRef { public string SystemPath { get; set; } public string PackFilePath { get; set; } - public string OverrideName { get; set; } = null; + public string? OverrideName { get; set; } = null; public FileRef(string systemPath, string packFilePath) { diff --git a/Shared/SharedCore/Shared.Core/Settings/ApplicationSettingsService.cs b/Shared/SharedCore/Shared.Core/Settings/ApplicationSettingsService.cs index 25fb5125d..d837b795e 100644 --- a/Shared/SharedCore/Shared.Core/Settings/ApplicationSettingsService.cs +++ b/Shared/SharedCore/Shared.Core/Settings/ApplicationSettingsService.cs @@ -14,11 +14,7 @@ public enum CameraControlMode public class ApplicationSettings { - public class GamePathPair - { - public GameTypeEnum Game { get; set; } - public string Path { get; set; } - } + public record GamePathPair(GameTypeEnum Game, string Path); public ObservableCollection RecentPackFilePaths { get; set; } = []; public ThemeType Theme { get; set; } = ThemeType.DarkTheme; @@ -30,7 +26,7 @@ public class GamePathPair public bool ShowCAWemFiles { get; set; } = false; public bool IsFirstTimeStartingApplication { get; set; } = true; public bool IsDeveloperRun { get; set; } = false; - public string WwisePath { get; set; } + public string WwisePath { get; set; } = string.Empty; public bool OnlyLoadLod0ForReferenceMeshes { get; set; } = true; public int VisualEditorsGridSize { get; set; } = 10; public Vector3 VertexSelectionColour { get; set; } = new Vector3(1.0f, 0.47f, 0.0f); @@ -39,7 +35,6 @@ public class GamePathPair public ApplicationSettings() { - // TODO: Need a way to get the right wwise path according to the game or instead just have several settings for the wwise CLI according tot he version var wwiseRoot = Environment.GetEnvironmentVariable("WWISEROOT") ?? ""; if (!string.IsNullOrEmpty(wwiseRoot)) diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/PackFileContainerLoaderTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/PackFileContainerLoaderTests.cs index 1f0c835db..61a55ec32 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/PackFileContainerLoaderTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/PackFileContainerLoaderTests.cs @@ -1,4 +1,4 @@ -using Moq; +using Moq; using Shared.Core.Misc; using Shared.Core.PackFiles.Utility; using Shared.Core.Services; @@ -36,8 +36,7 @@ public void Setup() _localizationManager = new LocalizationManager(); _settingsService = new ApplicationSettingsService(GameTypeEnum.Warhammer3); - _settingsService.CurrentSettings.GameDirectories.Add( - new ApplicationSettings.GamePathPair { Game = GameTypeEnum.Warhammer3, Path = _tempGameDir }); + _settingsService.CurrentSettings.GameDirectories.Add(new ApplicationSettings.GamePathPair(GameTypeEnum.Warhammer3, _tempGameDir)); } [TearDown] From f5f5024f76ebb7ce2b418e396e6a30781b524490 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 21:36:26 +0200 Subject: [PATCH 25/26] Shared.Core with warnings = error Code cleanup --- .../Exceptions/ExceptionInformation.cs | 2 +- .../ErrorHandling/UnkownXmlDataThrower.cs | 7 +++---- .../Events/Global/OpenEditorCommand.cs | 14 +++++++------- .../Shared.Core/Misc/JsonHelpers.cs | 2 +- .../SharedCore/Shared.Core/Misc/NotifyAttr.cs | 2 +- .../Models/Containers/PackFileContainer.cs | 19 +++++++++++++------ .../PackFiles/Models/IPackFileContainer.cs | 2 +- .../SharedCore/Shared.Core/Shared.Core.csproj | 1 + .../ToolCreation/EditorDatabase.cs | 2 +- .../ToolCreation/EditorInfoBuilder.cs | 11 +++++++---- .../ToolCreation/IEditorInterface.cs | 5 +---- 11 files changed, 37 insertions(+), 30 deletions(-) diff --git a/Shared/SharedCore/Shared.Core/ErrorHandling/Exceptions/ExceptionInformation.cs b/Shared/SharedCore/Shared.Core/ErrorHandling/Exceptions/ExceptionInformation.cs index d9e2c0e58..0be5a1ec7 100644 --- a/Shared/SharedCore/Shared.Core/ErrorHandling/Exceptions/ExceptionInformation.cs +++ b/Shared/SharedCore/Shared.Core/ErrorHandling/Exceptions/ExceptionInformation.cs @@ -2,7 +2,7 @@ namespace Shared.Core.ErrorHandling.Exceptions { - public record ExceptionPackFileContainerInfo(bool IsMainEditable, bool IsCa, string Name, string SystemPath); + public record ExceptionPackFileContainerInfo(bool IsMainEditable, bool IsCa, string Name, string? SystemPath); public record ExceptionInstance(string Message, string[] StackTrace); public record GraphicsResourceScopeInfo(string ScopeOwner, bool IsCurrentScope, int ResourceCount, List Resources); diff --git a/Shared/SharedCore/Shared.Core/ErrorHandling/UnkownXmlDataThrower.cs b/Shared/SharedCore/Shared.Core/ErrorHandling/UnkownXmlDataThrower.cs index d7eaf5adc..cb33f1e35 100644 --- a/Shared/SharedCore/Shared.Core/ErrorHandling/UnkownXmlDataThrower.cs +++ b/Shared/SharedCore/Shared.Core/ErrorHandling/UnkownXmlDataThrower.cs @@ -15,18 +15,17 @@ public UnknownXmlDataThrower() OnUnknownElement = Element, }; } - - void Attribute(object sender, XmlAttributeEventArgs e) + void Attribute(object? sender, XmlAttributeEventArgs e) { throw new XmlException("Unsuported xml attribute : " + e.Attr.LocalName + $" at line {e.LineNumber} and position {e.LinePosition}", null, e.LineNumber, e.LinePosition); } - void Node(object sender, XmlNodeEventArgs e) + void Node(object? sender, XmlNodeEventArgs e) { throw new XmlException("Unsuported xml node : " + e.LocalName + $" at line {e.LineNumber} and position {e.LinePosition}", null, e.LineNumber, e.LinePosition); } - void Element(object sender, XmlElementEventArgs e) + void Element(object? sender, XmlElementEventArgs e) { throw new XmlException("Unsuported xml element : " + e.Element.LocalName + $" at line {e.LineNumber} and position {e.LinePosition}", null, e.LineNumber, e.LinePosition); } diff --git a/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs b/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs index 3749b334f..15354e0a5 100644 --- a/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs +++ b/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs @@ -57,12 +57,12 @@ public void ExecuteAsWindow(string fileName, int width, int heigh) window.ShowDialog(); } - public void ExecuteAsWindow(EditorEnums preferedEditor, int width, int heigh) - { - var window = _editorCreator.CreateWindow(null, preferedEditor); - window.Width = width; - window.Height = heigh; - window.ShowDialog(); - } + //public void ExecuteAsWindow(EditorEnums preferedEditor, int width, int heigh) + //{ + // var window = _editorCreator.CreateWindow(null, preferedEditor); + // window.Width = width; + // window.Height = heigh; + // window.ShowDialog(); + //} } } diff --git a/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs b/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs index fa19347b0..74b396442 100644 --- a/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs +++ b/Shared/SharedCore/Shared.Core/Misc/JsonHelpers.cs @@ -18,7 +18,7 @@ public override bool CanConvert(Type objecType) return token.ToObject>(); } var tokenT = token.ToObject(); - return new List { tokenT }; + return new List { tokenT! }; } public override bool CanWrite diff --git a/Shared/SharedCore/Shared.Core/Misc/NotifyAttr.cs b/Shared/SharedCore/Shared.Core/Misc/NotifyAttr.cs index db66dcf8f..7ec1aa1f5 100644 --- a/Shared/SharedCore/Shared.Core/Misc/NotifyAttr.cs +++ b/Shared/SharedCore/Shared.Core/Misc/NotifyAttr.cs @@ -17,7 +17,7 @@ public NotifyAttr(T value) public NotifyAttr() { - _value = default; + _value = default!; } public NotifyAttr(T value, ValueChangedDelegate onValueChanged) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs index 33176d98a..e6f81c3ea 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/Containers/PackFileContainer.cs @@ -1,4 +1,5 @@ -using Shared.Core.Misc; +using CommunityToolkit.Diagnostics; +using Shared.Core.Misc; using Shared.Core.PackFiles.Models.FileSources; using Shared.Core.PackFiles.Serialization; using Shared.Core.PackFiles.Utility; @@ -11,12 +12,20 @@ internal class PackFileContainer : IPackFileContainerInternal public string Name { get; set; } public PFHeader Header { get; set; } public bool IsCaPackFile { get; set; } = false; - public string SystemFilePath { get; set; } + public string? SystemFilePath { get; set; } public long OriginalLoadByteSize { get; set; } = -1; public HashSet SourcePackFilePaths { get; set; } = []; public Dictionary FileList { get; set; } = []; + public PackFileContainer(string name) + { + Name = name; + var v = PackFileVersionConverter.ToString(PackFileVersion.PFH5); + Header = new PFHeader(v, PackFileCAType.MOD); + } + + public int GetFileCount() => FileList.Count; public void AddOrUpdateFile(string path, PackFile file) @@ -109,10 +118,7 @@ public List GetSubDirectories(string directoryPath) return results; } - public PackFileContainer(string name) - { - Name = name; - } + public void MergePackFileContainer(PackFileContainer other) { @@ -256,6 +262,7 @@ public virtual void SaveToDisk(string path, bool createBackup, GameInformation g if (OriginalLoadByteSize != -1) { + Guard.IsNotNull(SystemFilePath, "SystemFilePath must be set if saving to disk"); var fileInfo = new FileInfo(SystemFilePath); var byteSize = fileInfo.Length; if (byteSize != OriginalLoadByteSize) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs index 0742cd51f..04ec72cf6 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs @@ -4,7 +4,7 @@ public interface IPackFileContainer { string Name { get; } bool IsCaPackFile { get; set; } - string SystemFilePath { get; } + string? SystemFilePath { get; } int GetFileCount(); diff --git a/Shared/SharedCore/Shared.Core/Shared.Core.csproj b/Shared/SharedCore/Shared.Core/Shared.Core.csproj index 58e3a872a..13f699d45 100644 --- a/Shared/SharedCore/Shared.Core/Shared.Core.csproj +++ b/Shared/SharedCore/Shared.Core/Shared.Core.csproj @@ -4,6 +4,7 @@ net10.0-windows enable enable + true diff --git a/Shared/SharedCore/Shared.Core/ToolCreation/EditorDatabase.cs b/Shared/SharedCore/Shared.Core/ToolCreation/EditorDatabase.cs index be7703e0d..0ee51d78e 100644 --- a/Shared/SharedCore/Shared.Core/ToolCreation/EditorDatabase.cs +++ b/Shared/SharedCore/Shared.Core/ToolCreation/EditorDatabase.cs @@ -71,7 +71,7 @@ public IEditorInterface Create(EditorEnums editorEnum) return null; } - Type selectedEditor = null; + Type? selectedEditor = null; if (allEditors.Count == 1) { selectedEditor = allEditors.First().ViewModel; diff --git a/Shared/SharedCore/Shared.Core/ToolCreation/EditorInfoBuilder.cs b/Shared/SharedCore/Shared.Core/ToolCreation/EditorInfoBuilder.cs index 3beeb2ee9..7e64fefa4 100644 --- a/Shared/SharedCore/Shared.Core/ToolCreation/EditorInfoBuilder.cs +++ b/Shared/SharedCore/Shared.Core/ToolCreation/EditorInfoBuilder.cs @@ -30,12 +30,15 @@ public static class EditorPriorites public class EditorInfoBuilder { protected EditorInfo _instance; + + protected EditorInfoBuilder(EditorEnums editorType, Type tView, Type tViewModel) + { + _instance = new EditorInfo(editorType, tView, tViewModel); + } + public static EditorInfoBuilder Create(EditorEnums editorType) where TViewModel : IEditorInterface { - return new EditorInfoBuilder() - { - _instance = new EditorInfo(editorType, typeof(TView), typeof(TViewModel)) - }; + return new EditorInfoBuilder(editorType, typeof(TView), typeof(TViewModel)); } public EditorInfoBuilder AddToToolbar(string toolbarLabel, bool enabled = true) diff --git a/Shared/SharedCore/Shared.Core/ToolCreation/IEditorInterface.cs b/Shared/SharedCore/Shared.Core/ToolCreation/IEditorInterface.cs index 720652b4b..a85527ba9 100644 --- a/Shared/SharedCore/Shared.Core/ToolCreation/IEditorInterface.cs +++ b/Shared/SharedCore/Shared.Core/ToolCreation/IEditorInterface.cs @@ -42,9 +42,6 @@ public interface IEditorManager : IEditorCreator public void CloseAllTools(IEditorInterface tool); public void CloseToolsToLeft(IEditorInterface tool); public void CloseToolsToRight(IEditorInterface tool); - public bool Drop(IEditorInterface node, IEditorInterface targetNode = default, bool insertAfterTargetNode = default); - - - + public bool Drop(IEditorInterface node, IEditorInterface? targetNode = default, bool insertAfterTargetNode = default); } } From 2c44c0c321a8918d89929196a69a63dc097fd3bb Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Fri, 8 May 2026 21:51:27 +0200 Subject: [PATCH 26/26] Merged IEditorCreator --- AssetEditor/DependencyInjectionContainer.cs | 1 - .../DevConfig/CampaignAnimation_Damsel.cs | 4 ++-- .../MountAnimationCreator/DevConfig/MountTool.cs | 6 +++--- .../DevConfig/AnimTransfer_DwarfArcher.cs | 6 +++--- Editors/BmdEditor/DependencyInjectionContainer.cs | 2 +- Editors/BmdEditor/ViewModels/BmdSceneViewModel.cs | 4 ++-- .../MetaDataEditor/AnimationMeta/DevConfig/AnimMetaTool.cs | 4 ++-- .../MetaDataEditor/AnimationMeta/DevConfig/SuperView_Rat.cs | 6 +++--- .../Editor.VisualSkeletonEditor/DevConfig/SkeletonTool.cs | 4 ++-- Editors/TwuiEditor/Editor.Twui/DevConfig/TwuiToolEditor.cs | 4 ++-- .../Shared.Core/Events/Global/OpenEditorCommand.cs | 4 ++-- .../SharedCore/Shared.Core/ToolCreation/IEditorInterface.cs | 5 +---- 12 files changed, 23 insertions(+), 27 deletions(-) diff --git a/AssetEditor/DependencyInjectionContainer.cs b/AssetEditor/DependencyInjectionContainer.cs index bfcff145b..fe7785e28 100644 --- a/AssetEditor/DependencyInjectionContainer.cs +++ b/AssetEditor/DependencyInjectionContainer.cs @@ -19,7 +19,6 @@ public override void Register(IServiceCollection serviceCollection) { serviceCollection.AddScoped(); serviceCollection.AddScoped(); - serviceCollection.AddSingleton( x=> x.GetRequiredService()); serviceCollection.AddSingleton(); serviceCollection.AddTransient(); diff --git a/Editors/AnimationEditor/CampaignAnimationCreator/DevConfig/CampaignAnimation_Damsel.cs b/Editors/AnimationEditor/CampaignAnimationCreator/DevConfig/CampaignAnimation_Damsel.cs index 55b02a0f7..11b10ba71 100644 --- a/Editors/AnimationEditor/CampaignAnimationCreator/DevConfig/CampaignAnimation_Damsel.cs +++ b/Editors/AnimationEditor/CampaignAnimationCreator/DevConfig/CampaignAnimation_Damsel.cs @@ -9,10 +9,10 @@ namespace Editors.AnimationVisualEditors.CampaignAnimationCreator.DevConfig { internal class CampaignAnimation_Damsel : IDeveloperConfiguration { - private readonly IEditorCreator _editorCreator; + private readonly IEditorManager _editorCreator; private readonly IPackFileService _packFileService; - public CampaignAnimation_Damsel(IEditorCreator editorCreator, IPackFileService packFileService) + public CampaignAnimation_Damsel(IEditorManager editorCreator, IPackFileService packFileService) { _editorCreator = editorCreator; _packFileService = packFileService; diff --git a/Editors/AnimationEditor/MountAnimationCreator/DevConfig/MountTool.cs b/Editors/AnimationEditor/MountAnimationCreator/DevConfig/MountTool.cs index 24a98c173..3818c7eac 100644 --- a/Editors/AnimationEditor/MountAnimationCreator/DevConfig/MountTool.cs +++ b/Editors/AnimationEditor/MountAnimationCreator/DevConfig/MountTool.cs @@ -9,10 +9,10 @@ namespace Editors.AnimationVisualEditors.MountAnimationCreator.DevConfig { internal class MountTool : IDeveloperConfiguration { - private readonly IEditorCreator _editorCreator; + private readonly IEditorManager _editorCreator; private readonly IPackFileService _packFileService; - public MountTool(IEditorCreator editorCreator, IPackFileService packFileService) + public MountTool(IEditorManager editorCreator, IPackFileService packFileService) { _editorCreator = editorCreator; _packFileService = packFileService; @@ -30,7 +30,7 @@ public void OpenFileOnLoad() CreateLionAndHu01b(_editorCreator, _packFileService); } - static void CreateLionAndHu01b(IEditorCreator creator, IPackFileService packfileService) + static void CreateLionAndHu01b(IEditorManager creator, IPackFileService packfileService) { var riderInput = new AnimationToolInput() { diff --git a/Editors/AnimationReTarget/Editors.AnimatioReTarget/DevConfig/AnimTransfer_DwarfArcher.cs b/Editors/AnimationReTarget/Editors.AnimatioReTarget/DevConfig/AnimTransfer_DwarfArcher.cs index 8ffd7e1c3..9e9e99c35 100644 --- a/Editors/AnimationReTarget/Editors.AnimatioReTarget/DevConfig/AnimTransfer_DwarfArcher.cs +++ b/Editors/AnimationReTarget/Editors.AnimatioReTarget/DevConfig/AnimTransfer_DwarfArcher.cs @@ -9,10 +9,10 @@ namespace Editors.AnimatioReTarget.DevConfig { internal class AnimTransfer_DwarfArcher : IDeveloperConfiguration { - private readonly IEditorCreator _editorCreator; + private readonly IEditorManager _editorCreator; private readonly IPackFileService _packFileService; - public AnimTransfer_DwarfArcher(IEditorCreator editorCreator, IPackFileService packFileService) + public AnimTransfer_DwarfArcher(IEditorManager editorCreator, IPackFileService packFileService) { _editorCreator = editorCreator; _packFileService = packFileService; @@ -30,7 +30,7 @@ public void OpenFileOnLoad() CreateDwarfAndEmpArcher(_editorCreator, _packFileService); } - static void CreateDwarfAndEmpArcher(IEditorCreator creator, IPackFileService packfileService) + static void CreateDwarfAndEmpArcher(IEditorManager creator, IPackFileService packfileService) { var targetInput = new AnimationToolInput() { diff --git a/Editors/BmdEditor/DependencyInjectionContainer.cs b/Editors/BmdEditor/DependencyInjectionContainer.cs index 802dcd302..38bc98850 100644 --- a/Editors/BmdEditor/DependencyInjectionContainer.cs +++ b/Editors/BmdEditor/DependencyInjectionContainer.cs @@ -1,4 +1,4 @@ -using Editors.BmdEditor.ViewModels; +using Editors.BmdEditor.ViewModels; using Editors.BmdEditor.Views; using Editors.BmdEditor.Services; using Microsoft.Extensions.DependencyInjection; diff --git a/Editors/BmdEditor/ViewModels/BmdSceneViewModel.cs b/Editors/BmdEditor/ViewModels/BmdSceneViewModel.cs index 51ad26100..21a27ce36 100644 --- a/Editors/BmdEditor/ViewModels/BmdSceneViewModel.cs +++ b/Editors/BmdEditor/ViewModels/BmdSceneViewModel.cs @@ -24,7 +24,7 @@ public partial class BmdSceneViewModel : ObservableObject, IDisposable { private readonly ILogger _logger = Serilog.Log.ForContext(); private readonly IPackFileService _packFileService; - private readonly IEditorCreator _editorCreator; + private readonly IEditorManager _editorCreator; private readonly ResourceLibrary _resourceLibrary; private readonly IGraphicsResourceCreator _graphicsResourceCreator; private readonly MeshBuilderService _meshBuilderService; @@ -56,7 +56,7 @@ public IWpfGame? Scene3D public BmdSceneViewModel( IPackFileService packFileService, - IEditorCreator editorCreator, + IEditorManager editorCreator, ResourceLibrary resourceLibrary, IGraphicsResourceCreator graphicsResourceCreator, MeshBuilderService meshBuilderService, diff --git a/Editors/MetaDataEditor/AnimationMeta/DevConfig/AnimMetaTool.cs b/Editors/MetaDataEditor/AnimationMeta/DevConfig/AnimMetaTool.cs index c819c1279..48a0f57d2 100644 --- a/Editors/MetaDataEditor/AnimationMeta/DevConfig/AnimMetaTool.cs +++ b/Editors/MetaDataEditor/AnimationMeta/DevConfig/AnimMetaTool.cs @@ -9,11 +9,11 @@ namespace Editors.AnimationMeta.DevConfig { internal class AnimMetaTool : IDeveloperConfiguration { - private readonly IEditorCreator _editorCreator; + private readonly IEditorManager _editorCreator; private readonly IPackFileContainerLoader _packFileContainerLoader; private readonly IPackFileService _packFileService; - public AnimMetaTool(IEditorCreator editorCreator, IPackFileContainerLoader packFileContainerLoader, IPackFileService packFileService) + public AnimMetaTool(IEditorManager editorCreator, IPackFileContainerLoader packFileContainerLoader, IPackFileService packFileService) { _editorCreator = editorCreator; _packFileContainerLoader = packFileContainerLoader; diff --git a/Editors/MetaDataEditor/AnimationMeta/DevConfig/SuperView_Rat.cs b/Editors/MetaDataEditor/AnimationMeta/DevConfig/SuperView_Rat.cs index 921d590be..5ea470a3f 100644 --- a/Editors/MetaDataEditor/AnimationMeta/DevConfig/SuperView_Rat.cs +++ b/Editors/MetaDataEditor/AnimationMeta/DevConfig/SuperView_Rat.cs @@ -12,11 +12,11 @@ namespace Editors.AnimationMeta.DevConfig { internal class SuperView_Rat : IDeveloperConfiguration { - private readonly IEditorCreator _editorCreator; + private readonly IEditorManager _editorCreator; private readonly IPackFileContainerLoader _packFileContainerLoader; private readonly IPackFileService _packFileService; - public SuperView_Rat(IEditorCreator editorCreator, IPackFileContainerLoader packFileContainerLoader, IPackFileService packFileService) + public SuperView_Rat(IEditorManager editorCreator, IPackFileContainerLoader packFileContainerLoader, IPackFileService packFileService) { _editorCreator = editorCreator; _packFileContainerLoader = packFileContainerLoader; @@ -38,7 +38,7 @@ public void OpenFileOnLoad() CreateThrot(_editorCreator, _packFileService); } - void CreateThrot(IEditorCreator creator, IPackFileService packfileService) + void CreateThrot(IEditorManager creator, IPackFileService packfileService) { var debugInput = new AnimationToolInput() { diff --git a/Editors/SkeletonEditor/Editor.VisualSkeletonEditor/DevConfig/SkeletonTool.cs b/Editors/SkeletonEditor/Editor.VisualSkeletonEditor/DevConfig/SkeletonTool.cs index 0bba4a435..3bac1adc4 100644 --- a/Editors/SkeletonEditor/Editor.VisualSkeletonEditor/DevConfig/SkeletonTool.cs +++ b/Editors/SkeletonEditor/Editor.VisualSkeletonEditor/DevConfig/SkeletonTool.cs @@ -9,11 +9,11 @@ namespace Editor.VisualSkeletonEditor.DevConfig { internal class SkeletonTool : IDeveloperConfiguration { - private readonly IEditorCreator _editorCreator; + private readonly IEditorManager _editorCreator; private readonly IPackFileContainerLoader _packFileContainerLoader; private readonly IPackFileService _packFileService; - public SkeletonTool(IEditorCreator editorCreator, IPackFileContainerLoader packFileContainerLoader, IPackFileService packFileService) + public SkeletonTool(IEditorManager editorCreator, IPackFileContainerLoader packFileContainerLoader, IPackFileService packFileService) { _editorCreator = editorCreator; _packFileContainerLoader = packFileContainerLoader; diff --git a/Editors/TwuiEditor/Editor.Twui/DevConfig/TwuiToolEditor.cs b/Editors/TwuiEditor/Editor.Twui/DevConfig/TwuiToolEditor.cs index 28100d9ad..f2834efef 100644 --- a/Editors/TwuiEditor/Editor.Twui/DevConfig/TwuiToolEditor.cs +++ b/Editors/TwuiEditor/Editor.Twui/DevConfig/TwuiToolEditor.cs @@ -9,11 +9,11 @@ namespace Editors.Twui.DevConfig { internal class TwuiToolEditor : IDeveloperConfiguration { - private readonly IEditorCreator _editorCreator; + private readonly IEditorManager _editorCreator; private readonly IPackFileContainerLoader _packFileContainerLoader; private readonly IPackFileService _packFileService; - public TwuiToolEditor(IEditorCreator editorCreator, IPackFileContainerLoader packFileContainerLoader, IPackFileService packFileService) + public TwuiToolEditor(IEditorManager editorCreator, IPackFileContainerLoader packFileContainerLoader, IPackFileService packFileService) { _editorCreator = editorCreator; _packFileContainerLoader = packFileContainerLoader; diff --git a/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs b/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs index 15354e0a5..08d8582fb 100644 --- a/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs +++ b/Shared/SharedCore/Shared.Core/Events/Global/OpenEditorCommand.cs @@ -8,10 +8,10 @@ namespace Shared.Core.Events.Global { public class OpenEditorCommand : IUiCommand { - private readonly IEditorCreator _editorCreator; + private readonly IEditorManager _editorCreator; private readonly IPackFileService _packFileService; - public OpenEditorCommand(IEditorCreator editorCreator, IPackFileService packFileService) + public OpenEditorCommand(IEditorManager editorCreator, IPackFileService packFileService) { _packFileService = packFileService; _editorCreator = editorCreator; diff --git a/Shared/SharedCore/Shared.Core/ToolCreation/IEditorInterface.cs b/Shared/SharedCore/Shared.Core/ToolCreation/IEditorInterface.cs index a85527ba9..ba0e0546e 100644 --- a/Shared/SharedCore/Shared.Core/ToolCreation/IEditorInterface.cs +++ b/Shared/SharedCore/Shared.Core/ToolCreation/IEditorInterface.cs @@ -22,15 +22,12 @@ public interface IFileEditor public void LoadFile(PackFile file); } - public interface IEditorCreator + public interface IEditorManager { IEditorInterface CreateFromFile(PackFile file, EditorEnums? preferedEditor = null); IEditorInterface Create(EditorEnums editor, Action? onInitializeCallback = null); Window CreateWindow(PackFile packFile, EditorEnums? preferedEditor = null); - } - public interface IEditorManager : IEditorCreator - { IList GetAllEditors(); int GetCurrentEditor(); void SetEditorAsCurrent(IEditorInterface editor);