Resource Tree: Add ChangedItem-like icons, make UI prettier

This commit is contained in:
Exter-N 2023-09-01 06:06:42 +02:00
parent db521dd21c
commit 30c622c085
8 changed files with 139 additions and 95 deletions

View file

@ -11,20 +11,21 @@ using Penumbra.GameData.Structs;
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<nint, ResourceNode> 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<nint, ResourceNode> Nodes, EquipSlot Slot, CharacterArmor Equipment)
internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithUIData,
bool RedactExternalPaths, Dictionary<nint, ResourceNode> 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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
if (texNode == null)
continue;
if (WithNames)
if (WithUIData)
{
string? name = null;
if (shpk != null)
@ -259,7 +260,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
}
: 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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
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<string> array, Index index)

View file

@ -2,12 +2,14 @@ using System;
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<ResourceNode> 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 class ResourceNode
Children = new List<ResourceNode>();
}
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 class ResourceNode
Children = new List<ResourceNode>();
}
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 @@ public class ResourceNode
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);
}
}

View file

@ -56,12 +56,12 @@ public class ResourceTree
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 @@ public class ResourceTree
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 @@ public class ResourceTree
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<ResourceNode> nodes, ResolveContext context, Skeleton* skeleton, string prefix = "")
@ -133,11 +133,11 @@ public class ResourceTree
{
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);
}
}
}

View file

@ -29,34 +29,35 @@ public class ResourceTreeFactory
_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<ResourceTree>()
.ToArray();
}
public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters(
IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> 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 class ResourceTreeFactory
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;

View file

@ -554,7 +554,8 @@ public partial class ModEditWindow : Window, IDisposable
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 partial class ModEditWindow : Window, IDisposable
(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);
}

View file

@ -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<ResourceNode, Vector2> _drawActions;
@ -22,15 +23,16 @@ public class ResourceTreeViewer
private Task<ResourceTree[]>? _task;
public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, int actionCapacity, Action onRefresh,
Action<ResourceNode, Vector2> drawActions)
public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer,
int actionCapacity, Action onRefresh, Action<ResourceNode, Vector2> drawActions)
{
_config = config;
_treeFactory = treeFactory;
_actionCapacity = actionCapacity;
_onRefresh = onRefresh;
_drawActions = drawActions;
_unfolded = new HashSet<nint>();
_config = config;
_treeFactory = treeFactory;
_changedItemDrawer = changedItemDrawer;
_actionCapacity = actionCapacity;
_onRefresh = onRefresh;
_drawActions = drawActions;
_unfolded = new HashSet<nint>();
}
public void Draw()
@ -122,8 +124,24 @@ public class ResourceTreeViewer
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 @@ public class ResourceTreeViewer
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)
{

View file

@ -76,9 +76,11 @@ public class ChangedItemDrawer : IDisposable
/// <summary> Draw the icon corresponding to the category of a changed item. </summary>
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 @@ public class ChangedItemDrawer : IDisposable
}
/// <summary> Obtain the icon category corresponding to a changed item. </summary>
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 @@ public class ChangedItemDrawer : IDisposable
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,
};
/// <summary> Return more detailed object information in text, if it exists. </summary>
private static bool GetChangedItemObject(object? obj, out string text)
{

View file

@ -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<byte> Label