diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 53dedfa0..7ec75893 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -9,6 +9,7 @@ public class ResourceNode : ICloneable public string? Name; public string? FallbackName; public ChangedItemIcon Icon; + public ChangedItemIcon DescendentIcons; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -49,6 +50,7 @@ public class ResourceNode : ICloneable Name = other.Name; FallbackName = other.FallbackName; Icon = other.Icon; + DescendentIcons = other.DescendentIcons; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 0e3a92e2..b3b62359 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -171,6 +171,9 @@ public class ResourceTreeFactory { if (node.Name == parent?.Name) node.Name = null; + + if (parent != null) + parent.DescendentIcons |= node.Icon | node.DescendentIcons; }); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index ef847d8d..dc7fb55c 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -23,6 +23,10 @@ public class ResourceTreeViewer private readonly Action _drawActions; private readonly HashSet _unfolded; + private TreeCategory _categoryFilter; + private ChangedItemDrawer.ChangedItemIcon _typeFilter; + private string _nameFilter; + private Task? _task; public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, @@ -35,6 +39,10 @@ public class ResourceTreeViewer _onRefresh = onRefresh; _drawActions = drawActions; _unfolded = new HashSet(); + + _categoryFilter = AllCategories; + _typeFilter = ChangedItemDrawer.AllFlags; + _nameFilter = string.Empty; } public void Draw() @@ -42,6 +50,8 @@ public class ResourceTreeViewer if (ImGui.Button("Refresh Character List") || _task == null) _task = RefreshCharacterList(); + DrawFilters(); + using var child = ImRaii.Child("##Data"); if (!child) return; @@ -62,12 +72,11 @@ public class ResourceTreeViewer var debugMode = _config.DebugMode; foreach (var (tree, index) in _task.Result.WithIndex()) { - var headerColorId = - tree.LocalPlayerRelated ? ColorId.ResTreeLocalPlayer : - tree.PlayerRelated ? ColorId.ResTreePlayer : - tree.Networked ? ColorId.ResTreeNetworked : - ColorId.ResTreeNonNetworked; - using (var c = ImRaii.PushColor(ImGuiCol.Text, headerColorId.Value())) + var category = Classify(tree); + if (!_categoryFilter.HasFlag(category) || !tree.Name.Contains(_nameFilter)) + continue; + + using (var c = ImRaii.PushColor(ImGuiCol.Text, CategoryColor(category).Value())) { var isOpen = ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); if (debugMode) @@ -102,6 +111,34 @@ public class ResourceTreeViewer } } + private void DrawFilters() + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + + using (var id = ImRaii.PushId("TreeCategoryFilter")) + { + var spacing = ImGui.GetStyle().ItemInnerSpacing.X; + var categoryFilter = (uint)_categoryFilter; + foreach (var category in Enum.GetValues()) + { + ImGui.SameLine(0.0f, spacing); + using var c = ImRaii.PushColor(ImGuiCol.CheckMark, CategoryColor(category).Value()); + ImGui.CheckboxFlags($"##{category}", ref categoryFilter, (uint)category); + ImGuiUtil.HoverTooltip(CategoryFilterDescription(category)); + } + _categoryFilter = (TreeCategory)categoryFilter; + } + + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + + ImGui.SameLine(); + _changedItemDrawer.DrawTypeFilter(ref _typeFilter); + + ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); + } + private Task RefreshCharacterList() => Task.Run(() => { @@ -120,12 +157,27 @@ public class ResourceTreeViewer private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash) { + var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; + + NodeVisibility GetNodeVisibility(ResourceNode node) + { + if (node.Internal && !debugMode) + return NodeVisibility.Hidden; + + if (_typeFilter.HasFlag(node.Icon)) + return NodeVisibility.Visible; + if ((_typeFilter & node.DescendentIcons) != 0) + return NodeVisibility.DescendentsOnly; + return NodeVisibility.Hidden; + } + foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { - if (resourceNode.Internal && !debugMode) + var visibility = GetNodeVisibility(resourceNode); + if (visibility == NodeVisibility.Hidden) continue; var textColor = ImGui.GetColorU32(ImGuiCol.Text); @@ -140,9 +192,8 @@ public class ResourceTreeViewer var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { - var unfoldable = debugMode - ? resourceNode.Children.Count > 0 - : resourceNode.Children.Any(child => !child.Internal); + var hasVisibleChildren = resourceNode.Children.Any(child => GetNodeVisibility(child) != NodeVisibility.Hidden); + var unfoldable = hasVisibleChildren && visibility != NodeVisibility.DescendentsOnly; if (unfoldable) { using var font = ImRaii.PushFont(UiBuilder.IconFont); @@ -154,6 +205,11 @@ public class ResourceTreeViewer } else { + if (hasVisibleChildren && !unfolded) + { + _unfolded.Add(nodePathHash); + unfolded = true; + } ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); } @@ -200,7 +256,7 @@ public class ResourceTreeViewer ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); - ImGuiUtil.HoverTooltip($"{resourceNode.FullPath}\n\nClick to copy to clipboard."); + ImGuiUtil.HoverTooltip($"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard."); } else { @@ -223,4 +279,49 @@ public class ResourceTreeViewer DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31)); } } + + [Flags] + private enum TreeCategory : uint + { + LocalPlayer = 1, + Player = 2, + Networked = 4, + NonNetworked = 8, + } + + private const TreeCategory AllCategories = (TreeCategory)(((uint)TreeCategory.NonNetworked << 1) - 1); + + private static TreeCategory Classify(ResourceTree tree) + => tree.LocalPlayerRelated ? TreeCategory.LocalPlayer : + tree.PlayerRelated ? TreeCategory.Player : + tree.Networked ? TreeCategory.Networked : + TreeCategory.NonNetworked; + + private static ColorId CategoryColor(TreeCategory category) + => category switch + { + TreeCategory.LocalPlayer => ColorId.ResTreeLocalPlayer, + TreeCategory.Player => ColorId.ResTreePlayer, + TreeCategory.Networked => ColorId.ResTreeNetworked, + TreeCategory.NonNetworked => ColorId.ResTreeNonNetworked, + _ => throw new ArgumentException(), + }; + + private static string CategoryFilterDescription(TreeCategory category) + => category switch + { + TreeCategory.LocalPlayer => "Show you and what you own (mount, minion, accessory, pets and so on).", + TreeCategory.Player => "Show other players and what they own.", + TreeCategory.Networked => "Show non-player entities handled by the game server.", + TreeCategory.NonNetworked => "Show non-player entities handled locally.", + _ => throw new ArgumentException(), + }; + + [Flags] + private enum NodeVisibility : uint + { + Hidden = 0, + Visible = 1, + DescendentsOnly = 2, + } } diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 8916d365..ddd58a8a 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -145,6 +145,18 @@ public class ChangedItemDrawer : IDisposable if (_config.HideChangedItemFilters) return; + var typeFilter = _config.Ephemeral.ChangedItemFilter; + if (DrawTypeFilter(ref typeFilter)) + { + _config.Ephemeral.ChangedItemFilter = typeFilter; + _config.Ephemeral.Save(); + } + } + + /// Draw a header line with the different icon types to filter them. + public bool DrawTypeFilter(ref ChangedItemIcon typeFilter) + { + var ret = false; using var _ = ImRaii.PushId("ChangedItemIconFilter"); var size = new Vector2(2 * ImGui.GetTextLineHeight()); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); @@ -169,23 +181,24 @@ public class ChangedItemDrawer : IDisposable ChangedItemIcon.Unknown, }; - void DrawIcon(ChangedItemIcon type) + bool DrawIcon(ChangedItemIcon type, ref ChangedItemIcon typeFilter) { + var ret = false; var icon = _icons[type]; - var flag = _config.Ephemeral.ChangedItemFilter.HasFlag(type); + var flag = typeFilter.HasFlag(type); ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { - _config.Ephemeral.ChangedItemFilter = flag ? _config.Ephemeral.ChangedItemFilter & ~type : _config.Ephemeral.ChangedItemFilter | type; - _config.Ephemeral.Save(); + typeFilter = flag ? typeFilter & ~type : typeFilter | type; + ret = true; } using var popup = ImRaii.ContextPopupItem(type.ToString()); if (popup) if (ImGui.MenuItem("Enable Only This")) { - _config.Ephemeral.ChangedItemFilter = type; - _config.Ephemeral.Save(); + typeFilter = type; + ret = true; ImGui.CloseCurrentPopup(); } @@ -196,23 +209,27 @@ public class ChangedItemDrawer : IDisposable ImGui.SameLine(); ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0); } + + return ret; } foreach (var iconType in order) { - DrawIcon(iconType); + ret |= DrawIcon(iconType, ref typeFilter); ImGui.SameLine(); } ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, - _config.Ephemeral.ChangedItemFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : - _config.Ephemeral.ChangedItemFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); + typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : + typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); if (ImGui.IsItemClicked()) { - _config.Ephemeral.ChangedItemFilter = _config.Ephemeral.ChangedItemFilter == AllFlags ? 0 : AllFlags; - _config.Ephemeral.Save(); + typeFilter = typeFilter == AllFlags ? 0 : AllFlags; + ret = true; } + + return ret; } /// Obtain the icon category corresponding to a changed item.