using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.UiDebug.Browsing; using Dalamud.Interface.Internal.UiDebug.Utility; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Component.GUI; using static System.Globalization.NumberFormatInfo; using static Dalamud.Bindings.ImGui.ImGuiCol; using static Dalamud.Bindings.ImGui.ImGuiWindowFlags; using static Dalamud.Interface.FontAwesomeIcon; using static Dalamud.Interface.Internal.UiDebug.UiDebug; using static Dalamud.Interface.UiBuilder; using static Dalamud.Interface.Utility.ImGuiHelpers; using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags; // ReSharper disable StructLacksIEquatable.Global #pragma warning disable CS0659 namespace Dalamud.Interface.Internal.UiDebug; /// /// A tool that enables the user to select UI elements within the inspector by mousing over them onscreen. /// internal unsafe class ElementSelector : IDisposable { private const int UnitListCount = 18; private readonly UiDebug uiDebug; private string addressSearchInput = string.Empty; private int index; /// /// Initializes a new instance of the class. /// /// The instance of this Element Selector belongs to. internal ElementSelector(UiDebug uiDebug) { this.uiDebug = uiDebug; } /// /// Gets or sets the results retrieved by the Element Selector. /// internal static nint[] SearchResults { get; set; } = []; /// /// Gets or sets a value governing the highlighting of nodes when found via search. /// internal static float Countdown { get; set; } /// /// Gets or sets a value indicating whether the window has scrolled down to the position of the search result. /// internal static bool Scrolled { get; set; } /// /// Gets or sets a value indicating whether the mouseover UI is currently active. /// internal bool Active { get; set; } /// public void Dispose() { this.Active = false; } /// /// Draws the Element Selector and Address Search interface at the bottom of the sidebar. /// internal void DrawInterface() { using var ch = ImRaii.Child("###sidebar_elementSelector"u8, new(250, -1), true); if (ch.Success) { using (ImRaii.PushFont(IconFont)) { using (ImRaii.PushColor(Text, new Vector4(1, 1, 0.2f, 1), this.Active)) { if (ImGui.Button($"{(char)ObjectUngroup}")) { this.Active = !this.Active; } if (Countdown > 0) { Countdown -= 1; if (Countdown < 0) { Countdown = 0; } } } } if (ImGui.IsItemHovered()) { ImGui.SetTooltip("Element Selector"u8); } ImGui.SameLine(); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 32); ImGui.InputTextWithHint( "###addressSearchInput"u8, "Address Search"u8, ref this.addressSearchInput, 18, ImGuiInputTextFlags.AutoSelectAll); ImGui.SameLine(); if (ImGuiComponents.IconButton("###elemSelectorAddrSearch", Search) && nint.TryParse( this.addressSearchInput, NumberStyles.HexNumber | NumberStyles.AllowHexSpecifier, InvariantInfo, out var address)) { this.PerformSearch(address); } } } /// /// Draws the Element Selector's search output within the main window. /// internal void DrawSelectorOutput() { ImGui.GetIO().WantCaptureKeyboard = true; ImGui.GetIO().WantCaptureMouse = true; ImGui.GetIO().WantTextInput = true; if (ImGui.IsKeyPressed(ImGuiKey.Escape)) { this.Active = false; return; } ImGui.Text("ELEMENT SELECTOR"u8); ImGui.TextDisabled("Use the mouse to hover and identify UI elements, then click to jump to them in the inspector"u8); ImGui.TextDisabled("Use the scrollwheel to choose between overlapping elements"u8); ImGui.TextDisabled("Press ESCAPE to cancel"u8); ImGui.Spacing(); var mousePos = ImGui.GetMousePos() - MainViewport.Pos; var addonResults = GetAtkUnitBaseAtPosition(mousePos); using (ImRaii.PushColor(WindowBg, new Vector4(0.5f))) { using var ch = ImRaii.Child("noClick"u8, new(800, 2000), false, NoInputs | NoBackground | NoScrollWithMouse); if (ch.Success) { using var gr = ImRaii.Group(); Gui.PrintFieldValuePair("Mouse Position", $"{mousePos.X}, {mousePos.Y}"); ImGui.Spacing(); ImGui.Text("RESULTS:\n"u8); var i = 0; foreach (var a in addonResults) { var name = a.Addon->NameString; ImGui.Text($"[Addon] {name}"); using var indent = ImRaii.PushIndent(15.0f); foreach (var n in a.Nodes) { var nSelected = i++ == this.index; PrintNodeHeaderOnly(n.Node, nSelected, a.Addon); if (nSelected && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { this.Active = false; this.uiDebug.SelectedAddonName = a.Addon->NameString; var ptrList = new List { (nint)n.Node }; var nextNode = n.Node->ParentNode; while (nextNode != null) { ptrList.Add((nint)nextNode); nextNode = nextNode->ParentNode; } SearchResults = [.. ptrList]; Countdown = 100; Scrolled = false; } if (nSelected) { n.NodeBounds.DrawFilled(new(1, 1, 0.2f, 1)); } } } if (i != 0) { this.index -= (int)ImGui.GetIO().MouseWheel; while (this.index < 0) { this.index += i; } while (this.index >= i) { this.index -= i; } } } } } private static List GetAtkUnitBaseAtPosition(Vector2 position) { var addonResults = new List(); var unitListBaseAddr = GetUnitListBaseAddr(); if (unitListBaseAddr == null) { return addonResults; } foreach (var unit in UnitListOptions) { var unitManager = &unitListBaseAddr[unit.Index]; var safeCount = Math.Min(unitManager->Count, unitManager->Entries.Length); for (var i = 0; i < safeCount; i++) { var addon = unitManager->Entries[i].Value; if (addon == null || addon->RootNode == null) { continue; } if (!addon->IsVisible || !addon->RootNode->NodeFlags.HasFlag(Visible)) { continue; } var addonResult = new AddonResult(addon, []); if (addonResults.Contains(addonResult)) { continue; } if (addon->X > position.X || addon->Y > position.Y) { continue; } if (addon->X + addon->RootNode->Width < position.X) { continue; } if (addon->Y + addon->RootNode->Height < position.Y) { continue; } addonResult.Nodes.AddRange(GetNodeAtPosition(&addon->UldManager, position, true)); addonResults.Add(addonResult); } } return [.. addonResults.OrderBy(static w => w.Area)]; } private static List GetNodeAtPosition(AtkUldManager* uldManager, Vector2 position, bool reverse) { var nodeResults = new List(); for (var i = 0; i < uldManager->NodeListCount; i++) { var node = uldManager->NodeList[i]; var bounds = new NodeBounds(node); if (!bounds.ContainsPoint(position)) { continue; } if ((int)node->Type >= 1000) { var compNode = (AtkComponentNode*)node; nodeResults.AddRange(GetNodeAtPosition(&compNode->Component->UldManager, position, false)); } nodeResults.Add(new() { NodeBounds = bounds, Node = node }); } if (reverse) { nodeResults.Reverse(); } return nodeResults; } private static bool FindByAddress(AtkUnitBase* atkUnitBase, nint address) { if (atkUnitBase->RootNode == null) { return false; } if (!FindByAddress(atkUnitBase->RootNode, address, out var path)) { return false; } Scrolled = false; SearchResults = path?.ToArray() ?? []; Countdown = 100; return true; } private static bool FindByAddress(AtkResNode* node, nint address, out List? path) { if (node == null) { path = null; return false; } if ((nint)node == address) { path = [(nint)node]; return true; } if ((int)node->Type >= 1000) { var cNode = (AtkComponentNode*)node; if (cNode->Component != null) { if ((nint)cNode->Component == address) { path = [(nint)node]; return true; } if (FindByAddress(cNode->Component->UldManager.RootNode, address, out path) && path != null) { path.Add((nint)node); return true; } } } if (FindByAddress(node->ChildNode, address, out path) && path != null) { path.Add((nint)node); return true; } if (FindByAddress(node->PrevSiblingNode, address, out path) && path != null) { return true; } path = null; return false; } private static void PrintNodeHeaderOnly(AtkResNode* node, bool selected, AtkUnitBase* addon) { if (addon == null) { return; } if (node == null) { return; } var tree = AddonTree.GetOrCreate(addon->NameString); if (tree == null) { return; } using (ImRaii.PushColor(Text, selected ? new Vector4(1, 1, 0.2f, 1) : new(0.6f, 0.6f, 0.6f, 1))) { ResNodeTree.GetOrCreate(node, tree).WriteTreeHeading(); } } private void PerformSearch(nint address) { var unitListBaseAddr = GetUnitListBaseAddr(); if (unitListBaseAddr == null) { return; } for (var i = 0; i < UnitListCount; i++) { var unitManager = &unitListBaseAddr[i]; var safeCount = Math.Min(unitManager->Count, unitManager->Entries.Length); for (var j = 0; j < safeCount; j++) { var addon = unitManager->Entries[j].Value; if ((nint)addon == address || FindByAddress(addon, address)) { this.uiDebug.SelectedAddonName = addon->NameString; return; } } } } /// /// An found by the Element Selector. /// internal struct AddonResult { /// The addon itself. internal AtkUnitBase* Addon; /// A list of nodes discovered within this addon by the Element Selector. internal List Nodes; /// The calculated area of the addon's root node. internal float Area; /// /// Initializes a new instance of the struct. /// /// The addon found. /// A list for documenting nodes found within the addon. public AddonResult(AtkUnitBase* addon, List nodes) { this.Addon = addon; this.Nodes = nodes; var rootNode = addon->RootNode; this.Area = rootNode != null ? rootNode->Width * rootNode->Height * rootNode->ScaleY * rootNode->ScaleX : 0; } /// public override readonly bool Equals(object? obj) { if (obj is not AddonResult ar) { return false; } return (nint)this.Addon == (nint)ar.Addon; } } /// /// An found by the Element Selector. /// internal struct NodeResult { /// The node itself. internal AtkResNode* Node; /// A struct representing the perimeter of the node. internal NodeBounds NodeBounds; /// public override readonly bool Equals(object? obj) { if (obj is not NodeResult nr) { return false; } return nr.Node == this.Node; } } }