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/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 1ab1ed88..63ea8581 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -48,6 +48,8 @@ public partial class ModEditWindow return; } + if (DrawOptionSelectHeader()) + _quickImportActions.Clear(); _quickImportViewer.Draw(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index ce2bb2a4..db9201ca 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -381,18 +381,25 @@ public partial class ModEditWindow : Window, IDisposable } } - private void DrawOptionSelectHeader() + private bool DrawOptionSelectHeader() { const string defaultOption = "Default Option"; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); + var ret = false; if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", _editor.Option!.IsDefault)) + { _editor.LoadOption(-1, 0); + ret = true; + } ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) + { _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + ret = true; + } ImGui.SameLine(); ImGui.SetNextItemWidth(width.X); @@ -400,14 +407,19 @@ public partial class ModEditWindow : Window, IDisposable using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName); if (!combo) - return; + return ret; foreach (var (option, idx) in _mod!.AllSubMods.WithIndex()) { using var id = ImRaii.PushId(idx); if (ImGui.Selectable(option.FullName, option == _editor.Option)) + { _editor.LoadOption(option.GroupIdx, option.OptionIdx); + ret = true; + } } + + return ret; } private string _newSwapKey = string.Empty; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index ef847d8d..75f5c5b5 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,12 +39,16 @@ public class ResourceTreeViewer _onRefresh = onRefresh; _drawActions = drawActions; _unfolded = new HashSet(); + + _categoryFilter = AllCategories; + _typeFilter = ChangedItemDrawer.AllFlags; + _nameFilter = string.Empty; } public void Draw() { - if (ImGui.Button("Refresh Character List") || _task == null) - _task = RefreshCharacterList(); + DrawControls(); + _task ??= RefreshCharacterList(); using var child = ImRaii.Child("##Data"); if (!child) @@ -62,12 +70,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, StringComparison.OrdinalIgnoreCase)) + 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 +109,40 @@ public class ResourceTreeViewer } } + private void DrawControls() + { + var yOffset = (ChangedItemDrawer.TypeFilterIconSize.Y - ImGui.GetFrameHeight()) / 2f; + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + yOffset); + + if (ImGui.Button("Refresh Character List")) + _task = RefreshCharacterList(); + + 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, -yOffset); + + ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); + } + private Task RefreshCharacterList() => Task.Run(() => { @@ -120,12 +161,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 +196,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 +209,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 +260,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 +283,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..0a1d58f9 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -52,6 +52,9 @@ public class ChangedItemDrawer : IDisposable private readonly Dictionary _icons = new(16); private float _smallestIconWidth; + public static Vector2 TypeFilterIconSize + => new(2 * ImGui.GetTextLineHeight()); + public ChangedItemDrawer(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) { @@ -145,8 +148,20 @@ public class ChangedItemDrawer : IDisposable if (_config.HideChangedItemFilters) return; + var typeFilter = _config.Ephemeral.ChangedItemFilter; + if (DrawTypeFilter(ref typeFilter, 0.0f)) + { + _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, float yOffset) + { + var ret = false; using var _ = ImRaii.PushId("ChangedItemIconFilter"); - var size = new Vector2(2 * ImGui.GetTextLineHeight()); + var size = TypeFilterIconSize; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); var order = new[] { @@ -169,23 +184,25 @@ 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.SetCursorPosY(ImGui.GetCursorPosY() + yOffset); 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 +213,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.SetCursorPos(new(ImGui.GetContentRegionMax().X - size.X, ImGui.GetCursorPosY() + yOffset)); 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.