From 30c622c085d658ca69bf980a707ab1860b9993f8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 1 Sep 2023 06:06:42 +0200 Subject: [PATCH] Resource Tree: Add ChangedItem-like icons, make UI prettier --- .../Interop/ResourceTree/ResolveContext.cs | 72 ++++++++++--------- Penumbra/Interop/ResourceTree/ResourceNode.cs | 31 +++++--- Penumbra/Interop/ResourceTree/ResourceTree.cs | 20 +++--- .../ResourceTree/ResourceTreeFactory.cs | 17 ++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 5 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 44 ++++++++---- Penumbra/UI/ChangedItemDrawer.cs | 41 ++++++----- Penumbra/UI/Tabs/OnScreenTab.cs | 4 +- 8 files changed, 139 insertions(+), 95 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index a97ff726..72a5be69 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -11,20 +11,21 @@ using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; -using static Penumbra.GameData.Files.ShpkFile; +using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; -internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames) +internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, + bool RedactExternalPaths) { public readonly Dictionary Nodes = new(128); public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment) - => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, Nodes, slot, equipment); + => new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithUIData, RedactExternalPaths, Nodes, slot, equipment); } -internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, - Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) +internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData, + bool RedactExternalPaths, Dictionary Nodes, EquipSlot Slot, CharacterArmor Equipment) { private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal) @@ -78,7 +79,7 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (fullPath.InternalName.IsEmpty) fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath); - var node = new ResourceNode(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); + var node = new ResourceNode(default, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal); if (resourceHandle != null) Nodes.Add((nint)resourceHandle, node); @@ -99,14 +100,14 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec gamePaths = Filter(gamePaths); if (gamePaths.Count == 1) - return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, objectAddress, (nint)handle, gamePaths[0], fullPath, + return new ResourceNode(withName ? GuessUIDataFromPath(gamePaths[0]) : default, type, objectAddress, (nint)handle, gamePaths[0], fullPath, GetResourceHandleLength(handle), @internal); Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:"); foreach (var gamePath in gamePaths) Penumbra.Log.Information($"Game path: {gamePath}"); - return new ResourceNode(null, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); + return new ResourceNode(default, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal); } public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr) @@ -129,10 +130,10 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (node == null) return null; - if (WithNames) + if (WithUIData) { - var name = GuessModelName(node.GamePath); - node = node.WithName(name != null ? $"IMC: {name}" : null); + var uiData = GuessModelUIData(node.GamePath); + node = node.WithUIData(uiData.PrependName("IMC: ")); } Nodes.Add((nint)imc, node); @@ -145,7 +146,7 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (Nodes.TryGetValue((nint)tex, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithUIData); if (node != null) Nodes.Add((nint)tex, node); @@ -164,8 +165,8 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec if (node == null) return null; - if (WithNames) - node = node.WithName(GuessModelName(node.GamePath)); + if (WithUIData) + node = node.WithUIData(GuessModelUIData(node.GamePath)); for (var i = 0; i < mdl->MaterialCount; i++) { @@ -173,9 +174,9 @@ private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objec var mtrlNode = CreateNodeFromMaterial(mtrl); if (mtrlNode != null) // Don't keep the material's name if it's redundant with the model's name. - node.Children.Add(WithNames - ? mtrlNode.WithName((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) - ?? $"Material #{i}") + node.Children.Add(WithUIData + ? mtrlNode.WithUIData((string.Equals(mtrlNode.Name, node.Name, StringComparison.Ordinal) ? null : mtrlNode.Name) + ?? $"Material #{i}", mtrlNode.Icon) : mtrlNode); } @@ -225,15 +226,15 @@ static ushort GetTextureIndex(ushort texFlags) if (Nodes.TryGetValue((nint)resource, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithUIData); if (node == null) return null; var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false); if (shpkNode != null) - node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode); - var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var shpk = WithNames && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + node.Children.Add(WithUIData ? shpkNode.WithUIData("Shader Package", 0) : shpkNode); + var shpkFile = WithUIData && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; + var shpk = WithUIData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; for (var i = 0; i < resource->NumTex; i++) { @@ -241,7 +242,7 @@ static ushort GetTextureIndex(ushort texFlags) if (texNode == null) continue; - if (WithNames) + if (WithUIData) { string? name = null; if (shpk != null) @@ -259,7 +260,7 @@ static ushort GetTextureIndex(ushort texFlags) name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; } } - node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); + node.Children.Add(texNode.WithUIData(name ?? $"Texture #{i}", 0)); } else { @@ -280,7 +281,7 @@ static ushort GetTextureIndex(ushort texFlags) if (Nodes.TryGetValue((nint)sklb->SkeletonResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithUIData); if (node != null) Nodes.Add((nint)sklb->SkeletonResourceHandle, node); @@ -295,7 +296,7 @@ static ushort GetTextureIndex(ushort texFlags) if (Nodes.TryGetValue((nint)sklb->SkeletonParameterResourceHandle, out var cached)) return cached; - var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithNames); + var node = CreateNodeFromResourceHandle(ResourceType.Skp, (nint)sklb, (ResourceHandle*)sklb->SkeletonParameterResourceHandle, true, WithUIData); if (node != null) Nodes.Add((nint)sklb->SkeletonParameterResourceHandle, node); @@ -308,7 +309,7 @@ private FullPath FilterFullPath(FullPath fullPath) return fullPath; var relPath = Path.GetRelativePath(Config.ModDirectory, fullPath.FullName); - if (relPath == "." || !relPath.StartsWith('.') && !Path.IsPathRooted(relPath)) + if (!RedactExternalPaths || relPath == "." || !relPath.StartsWith('.') && !Path.IsPathRooted(relPath)) return fullPath.Exists ? fullPath : FullPath.Empty; return FullPath.Empty; @@ -351,7 +352,7 @@ private List Filter(List gamePaths) } : false; - private string? GuessModelName(Utf8GamePath gamePath) + private ResourceNode.UIData GuessModelUIData(Utf8GamePath gamePath) { var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); // Weapons intentionally left out. @@ -359,23 +360,24 @@ private List Filter(List gamePaths) if (isEquipment) foreach (var item in Identifier.Identify(Equipment.Set, Equipment.Variant, Slot.ToSlot())) { - return Slot switch + var name = Slot switch { EquipSlot.RFinger => "R: ", EquipSlot.LFinger => "L: ", _ => string.Empty, } + item.Name.ToString(); + return new(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); } - var nameFromPath = GuessNameFromPath(gamePath); - if (nameFromPath != null) - return nameFromPath; + var dataFromPath = GuessUIDataFromPath(gamePath); + if (dataFromPath.Name != null) + return dataFromPath; - return isEquipment ? Slot.ToName() : null; + return isEquipment ? new(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) : new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } - private string? GuessNameFromPath(Utf8GamePath gamePath) + private ResourceNode.UIData GuessUIDataFromPath(Utf8GamePath gamePath) { foreach (var obj in Identifier.Identify(gamePath.ToString())) { @@ -383,10 +385,10 @@ private List Filter(List gamePaths) if (name.StartsWith("Customization:")) name = name[14..].Trim(); if (name != "Unknown") - return name; + return new(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); } - return null; + return new(null, ChangedItemDrawer.ChangedItemIcon.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index bceda36c..17584787 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using Penumbra.GameData.Enums; using Penumbra.String.Classes; +using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; namespace Penumbra.Interop.ResourceTree; public class ResourceNode { public readonly string? Name; + public readonly ChangedItemIcon Icon; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -18,9 +20,11 @@ public class ResourceNode public readonly bool Internal; public readonly List Children; - public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, ulong length, bool @internal) + public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, + ulong length, bool @internal) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; @@ -35,10 +39,11 @@ public ResourceNode(string? name, ResourceType type, nint objectAddress, nint re Children = new List(); } - public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, + public ResourceNode(UIData uiData, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath, ulong length, bool @internal) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; @@ -50,9 +55,10 @@ public ResourceNode(string? name, ResourceType type, nint objectAddress, nint re Children = new List(); } - private ResourceNode(string? name, ResourceNode originalResourceNode) + private ResourceNode(UIData uiData, ResourceNode originalResourceNode) { - Name = name; + Name = uiData.Name; + Icon = uiData.Icon; Type = originalResourceNode.Type; ObjectAddress = originalResourceNode.ObjectAddress; ResourceHandle = originalResourceNode.ResourceHandle; @@ -64,6 +70,15 @@ private ResourceNode(string? name, ResourceNode originalResourceNode) Children = originalResourceNode.Children; } - public ResourceNode WithName(string? name) - => string.Equals(Name, name, StringComparison.Ordinal) ? this : new ResourceNode(name, this); + public ResourceNode WithUIData(string? name, ChangedItemIcon icon) + => string.Equals(Name, name, StringComparison.Ordinal) && Icon == icon ? this : new ResourceNode(new(name, icon), this); + + public ResourceNode WithUIData(UIData uiData) + => string.Equals(Name, uiData.Name, StringComparison.Ordinal) && Icon == uiData.Icon ? this : new ResourceNode(uiData, this); + + public readonly record struct UIData(string? Name, ChangedItemIcon Icon) + { + public readonly UIData PrependName(string prefix) + => Name == null ? this : new(prefix + Name, Icon); + } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index f3e6ca51..c6e90c6b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -56,12 +56,12 @@ internal unsafe void LoadResources(GlobalResolveContext globalContext) var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = context.CreateNodeFromImc(imc); if (imcNode != null) - Nodes.Add(globalContext.WithNames ? imcNode.WithName(imcNode.Name ?? $"IMC #{i}") : imcNode); + Nodes.Add(globalContext.WithUIData ? imcNode.WithUIData(imcNode.Name ?? $"IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)model->Models[i]; var mdlNode = context.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode); + Nodes.Add(globalContext.WithUIData ? mdlNode.WithUIData(mdlNode.Name ?? $"Model #{i}", mdlNode.Icon) : mdlNode); } AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton); @@ -92,15 +92,15 @@ private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanE var imc = (ResourceHandle*)subObject->IMCArray[i]; var imcNode = subObjectContext.CreateNodeFromImc(imc); if (imcNode != null) - subObjectNodes.Add(globalContext.WithNames - ? imcNode.WithName(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}") + subObjectNodes.Add(globalContext.WithUIData + ? imcNode.WithUIData(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}", imcNode.Icon) : imcNode); var mdl = (RenderModel*)subObject->Models[i]; var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl); if (mdlNode != null) - subObjectNodes.Add(globalContext.WithNames - ? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}") + subObjectNodes.Add(globalContext.WithUIData + ? mdlNode.WithUIData(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}", mdlNode.Icon) : mdlNode); } @@ -117,11 +117,11 @@ private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanE var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal); if (decalNode != null) - Nodes.Add(globalContext.WithNames ? decalNode.WithName(decalNode.Name ?? "Face Decal") : decalNode); + Nodes.Add(globalContext.WithUIData ? decalNode.WithUIData(decalNode.Name ?? "Face Decal", decalNode.Icon) : decalNode); var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal); if (legacyDecalNode != null) - Nodes.Add(globalContext.WithNames ? legacyDecalNode.WithName(legacyDecalNode.Name ?? "Legacy Body Decal") : legacyDecalNode); + Nodes.Add(globalContext.WithUIData ? legacyDecalNode.WithUIData(legacyDecalNode.Name ?? "Legacy Body Decal", legacyDecalNode.Icon) : legacyDecalNode); } private unsafe void AddSkeleton(List nodes, ResolveContext context, Skeleton* skeleton, string prefix = "") @@ -133,11 +133,11 @@ private unsafe void AddSkeleton(List nodes, ResolveContext context { var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (sklbNode != null) - nodes.Add(context.WithNames ? sklbNode.WithName($"{prefix}Skeleton #{i}") : sklbNode); + nodes.Add(context.WithUIData ? sklbNode.WithUIData($"{prefix}Skeleton #{i}", sklbNode.Icon) : sklbNode); var skpNode = context.CreateParameterNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]); if (skpNode != null) - nodes.Add(context.WithNames ? skpNode.WithName($"{prefix}Skeleton #{i} Parameters") : skpNode); + nodes.Add(context.WithUIData ? skpNode.WithUIData($"{prefix}Skeleton #{i} Parameters", skpNode.Icon) : skpNode); } } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index f416dc12..8d5318f2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -29,34 +29,35 @@ public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, Collecti _actors = actors; } - public ResourceTree[] FromObjectTable(bool withNames = true) + public ResourceTree[] FromObjectTable(bool withNames = true, bool redactExternalPaths = true) { var cache = new TreeBuildCache(_objects, _gameData); return cache.Characters - .Select(c => FromCharacter(c, cache, withNames)) + .Select(c => FromCharacter(c, cache, withNames, redactExternalPaths)) .OfType() .ToArray(); } public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, - bool withNames = true) + bool withUIData = true, bool redactExternalPaths = true) { var cache = new TreeBuildCache(_objects, _gameData); foreach (var character in characters) { - var tree = FromCharacter(character, cache, withNames); + var tree = FromCharacter(character, cache, withUIData, redactExternalPaths); if (tree != null) yield return (character, tree); } } - public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withNames = true) - => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withNames); + public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withUIData = true, + bool redactExternalPaths = true) + => FromCharacter(character, new TreeBuildCache(_objects, _gameData), withUIData, redactExternalPaths); private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, - bool withNames = true) + bool withUIData = true, bool redactExternalPaths = true) { if (!character.IsValid()) return null; @@ -73,7 +74,7 @@ public ResourceTree[] FromObjectTable(bool withNames = true) var (name, related) = GetCharacterName(character, cache); var tree = new ResourceTree(name, (nint)gameObjStruct, (nint)drawObjStruct, related, collectionResolveData.ModCollection.Name); var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection, - ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withNames); + ((Character*)gameObjStruct)->CharacterData.ModelCharaId, withUIData, redactExternalPaths); tree.LoadResources(globalContext); tree.FlatNodes.UnionWith(globalContext.Nodes.Values); return tree; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e90c148e..890abfed 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -554,7 +554,8 @@ private HashSet FindPathsStartingWith(ByteString prefix) public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents) + CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents, + ChangedItemDrawer changedItemDrawer) : base(WindowBaseLabel) { _performance = performance; @@ -581,7 +582,7 @@ public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialo (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); - _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 8e996eb7..528f19c1 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -14,7 +14,8 @@ namespace Penumbra.UI.AdvancedWindow; public class ResourceTreeViewer { private readonly Configuration _config; - private readonly ResourceTreeFactory _treeFactory; + private readonly ResourceTreeFactory _treeFactory; + private readonly ChangedItemDrawer _changedItemDrawer; private readonly int _actionCapacity; private readonly Action _onRefresh; private readonly Action _drawActions; @@ -22,15 +23,16 @@ public class ResourceTreeViewer private Task? _task; - public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, int actionCapacity, Action onRefresh, - Action drawActions) + public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, + int actionCapacity, Action onRefresh, Action drawActions) { - _config = config; - _treeFactory = treeFactory; - _actionCapacity = actionCapacity; - _onRefresh = onRefresh; - _drawActions = drawActions; - _unfolded = new HashSet(); + _config = config; + _treeFactory = treeFactory; + _changedItemDrawer = changedItemDrawer; + _actionCapacity = actionCapacity; + _onRefresh = onRefresh; + _drawActions = drawActions; + _unfolded = new HashSet(); } public void Draw() @@ -122,8 +124,24 @@ private void DrawNodes(IEnumerable resourceNodes, int level, nint ImGui.TableNextColumn(); var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) - { - ImGui.TableHeader((resourceNode.Children.Count > 0 ? unfolded ? "[-] " : "[+] " : string.Empty) + resourceNode.Name); + { + if (resourceNode.Children.Count > 0) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var icon = (unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(); + var offset = (ImGui.GetFrameHeight() - ImGui.CalcTextSize(icon).X) / 2; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset); + ImGui.TextUnformatted(icon); + ImGui.SameLine(0f, offset + ImGui.GetStyle().ItemInnerSpacing.X); + } + else + { + ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); + ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); + } + _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); + ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.TableHeader(resourceNode.Name); if (ImGui.IsItemClicked() && resourceNode.Children.Count > 0) { if (unfolded) @@ -170,7 +188,9 @@ private void DrawNodes(IEnumerable resourceNodes, int level, nint ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in."); - } + } + + mutedColor.Dispose(); if (_actionCapacity > 0) { diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 7a81ec60..da4faa43 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -76,9 +76,11 @@ public bool FilterChangedItem(string name, object? data, LowerString filter) /// Draw the icon corresponding to the category of a changed item. public void DrawCategoryIcon(string name, object? data) + => DrawCategoryIcon(GetCategoryIcon(name, data)); + + public void DrawCategoryIcon(ChangedItemIcon iconType) { - var height = ImGui.GetFrameHeight(); - var iconType = GetCategoryIcon(name, data); + var height = ImGui.GetFrameHeight(); if (!_icons.TryGetValue(iconType, out var icon)) { ImGui.Dummy(new Vector2(height)); @@ -216,27 +218,13 @@ void DrawIcon(ChangedItemIcon type) } /// Obtain the icon category corresponding to a changed item. - private static ChangedItemIcon GetCategoryIcon(string name, object? obj) + internal static ChangedItemIcon GetCategoryIcon(string name, object? obj) { var iconType = ChangedItemIcon.Unknown; switch (obj) { case EquipItem it: - iconType = it.Type.ToSlot() switch - { - EquipSlot.MainHand => ChangedItemIcon.Mainhand, - EquipSlot.OffHand => ChangedItemIcon.Offhand, - EquipSlot.Head => ChangedItemIcon.Head, - EquipSlot.Body => ChangedItemIcon.Body, - EquipSlot.Hands => ChangedItemIcon.Hands, - EquipSlot.Legs => ChangedItemIcon.Legs, - EquipSlot.Feet => ChangedItemIcon.Feet, - EquipSlot.Ears => ChangedItemIcon.Ears, - EquipSlot.Neck => ChangedItemIcon.Neck, - EquipSlot.Wrists => ChangedItemIcon.Wrists, - EquipSlot.RFinger => ChangedItemIcon.Finger, - _ => ChangedItemIcon.Unknown, - }; + iconType = GetCategoryIcon(it.Type.ToSlot()); break; case ModelChara m: iconType = (CharacterBase.ModelType)m.Type switch @@ -259,6 +247,23 @@ private static ChangedItemIcon GetCategoryIcon(string name, object? obj) return iconType; } + internal static ChangedItemIcon GetCategoryIcon(EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => ChangedItemIcon.Mainhand, + EquipSlot.OffHand => ChangedItemIcon.Offhand, + EquipSlot.Head => ChangedItemIcon.Head, + EquipSlot.Body => ChangedItemIcon.Body, + EquipSlot.Hands => ChangedItemIcon.Hands, + EquipSlot.Legs => ChangedItemIcon.Legs, + EquipSlot.Feet => ChangedItemIcon.Feet, + EquipSlot.Ears => ChangedItemIcon.Ears, + EquipSlot.Neck => ChangedItemIcon.Neck, + EquipSlot.Wrists => ChangedItemIcon.Wrists, + EquipSlot.RFinger => ChangedItemIcon.Finger, + _ => ChangedItemIcon.Unknown, + }; + /// Return more detailed object information in text, if it exists. private static bool GetChangedItemObject(object? obj, out string text) { diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 0ebc7dbd..c8f86333 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -10,10 +10,10 @@ public class OnScreenTab : ITab private readonly Configuration _config; private ResourceTreeViewer _viewer; - public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory) + public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer) { _config = config; - _viewer = new ResourceTreeViewer(_config, treeFactory, 0, delegate { }, delegate { }); + _viewer = new ResourceTreeViewer(_config, treeFactory, changedItemDrawer, 0, delegate { }, delegate { }); } public ReadOnlySpan Label