Dalamud/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
Haselnussbomber 58fbff7c56
Update text-related ImGui calls (#2337)
* Update text-related ImGui calls

* Use ImU8String for SafeTextColored

* Restore wrapped calls

* Update MenuItem call

* Use ImGui.Text over ImGui.TextUnformatted

* Add ImGui.TextColoredWrapped

* Obsolete SafeText helpers

* Fix obsoleted calls

* SafeTextColored didn't exist before imgui-bindings

* Remove %% replacements
2025-08-04 15:46:43 -07:00

494 lines
15 KiB
C#

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.UiDebug2.Browsing;
using Dalamud.Interface.Internal.UiDebug2.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.UiDebug2.UiDebug2;
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.UiDebug2;
/// <summary>
/// A tool that enables the user to select UI elements within the inspector by mousing over them onscreen.
/// </summary>
internal unsafe class ElementSelector : IDisposable
{
private const int UnitListCount = 18;
private readonly UiDebug2 uiDebug2;
private string addressSearchInput = string.Empty;
private int index;
/// <summary>
/// Initializes a new instance of the <see cref="ElementSelector"/> class.
/// </summary>
/// <param name="uiDebug2">The instance of <see cref="UiDebug2"/> this Element Selector belongs to.</param>
internal ElementSelector(UiDebug2 uiDebug2)
{
this.uiDebug2 = uiDebug2;
}
/// <summary>
/// Gets or sets the results retrieved by the Element Selector.
/// </summary>
internal static nint[] SearchResults { get; set; } = [];
/// <summary>
/// Gets or sets a value governing the highlighting of nodes when found via search.
/// </summary>
internal static float Countdown { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the window has scrolled down to the position of the search result.
/// </summary>
internal static bool Scrolled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the mouseover UI is currently active.
/// </summary>
internal bool Active { get; set; }
/// <inheritdoc/>
public void Dispose()
{
this.Active = false;
}
/// <summary>
/// Draws the Element Selector and Address Search interface at the bottom of the sidebar.
/// </summary>
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);
}
}
}
/// <summary>
/// Draws the Element Selector's search output within the main window.
/// </summary>
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();
if (gr.Success)
{
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}");
ImGui.Indent(15);
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.uiDebug2.SelectedAddonName = a.Addon->NameString;
var ptrList = new List<nint> { (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));
}
}
ImGui.Indent(-15);
}
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<AddonResult> GetAtkUnitBaseAtPosition(Vector2 position)
{
var addonResults = new List<AddonResult>();
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<NodeResult> GetNodeAtPosition(AtkUldManager* uldManager, Vector2 position, bool reverse)
{
var nodeResults = new List<NodeResult>();
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<nint>? 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.uiDebug2.SelectedAddonName = addon->NameString;
return;
}
}
}
}
/// <summary>
/// An <see cref="AtkUnitBase"/> found by the Element Selector.
/// </summary>
internal struct AddonResult
{
/// <summary>The addon itself.</summary>
internal AtkUnitBase* Addon;
/// <summary>A list of nodes discovered within this addon by the Element Selector.</summary>
internal List<NodeResult> Nodes;
/// <summary>The calculated area of the addon's root node.</summary>
internal float Area;
/// <summary>
/// Initializes a new instance of the <see cref="AddonResult"/> struct.
/// </summary>
/// <param name="addon">The addon found.</param>
/// <param name="nodes">A list for documenting nodes found within the addon.</param>
public AddonResult(AtkUnitBase* addon, List<NodeResult> nodes)
{
this.Addon = addon;
this.Nodes = nodes;
var rootNode = addon->RootNode;
this.Area = rootNode != null ? rootNode->Width * rootNode->Height * rootNode->ScaleY * rootNode->ScaleX : 0;
}
/// <inheritdoc/>
public override readonly bool Equals(object? obj)
{
if (obj is not AddonResult ar)
{
return false;
}
return (nint)this.Addon == (nint)ar.Addon;
}
}
/// <summary>
/// An <see cref="AtkResNode"/> found by the Element Selector.
/// </summary>
internal struct NodeResult
{
/// <summary>The node itself.</summary>
internal AtkResNode* Node;
/// <summary>A struct representing the perimeter of the node.</summary>
internal NodeBounds NodeBounds;
/// <inheritdoc/>
public override readonly bool Equals(object? obj)
{
if (obj is not NodeResult nr)
{
return false;
}
return nr.Node == this.Node;
}
}
}