mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-02 22:14:35 +01:00
* 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
494 lines
15 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|