Initial commit: AetherBags + KamiToolKit for FC Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase : IDisposable {
|
||||
|
||||
internal const uint NodeIdBase = 100_000_000;
|
||||
protected static readonly List<NodeBase> CreatedNodes = [];
|
||||
private static int logIndent = -1;
|
||||
|
||||
internal static uint CurrentOffset;
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
internal abstract AtkResNode* ResNode { get; }
|
||||
internal bool IsAddonRootNode;
|
||||
|
||||
private delegate* unmanaged<AtkResNode*, bool, void> originalDestructorFunction;
|
||||
private AtkResNode.Delegates.Destroy destructorFunction = null!;
|
||||
private AtkResNode.AtkResNodeVirtualTable* virtualTable;
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
logIndent++;
|
||||
LogIndented($"Beginning Dispose for {GetType()}");
|
||||
logIndent++;
|
||||
|
||||
if (MainThreadSafety.TryAssertMainThread()) {
|
||||
if (Framework.Instance()->IsDestroying) {
|
||||
LogIndented("Game is shutting down, aborting manual dispose.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDisposed) {
|
||||
LogIndented("Node was already disposed, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
isDisposed = true;
|
||||
|
||||
if (!IsNodeValid()) {
|
||||
Log.Warning("Invalid node, dispose aborted.");
|
||||
return;
|
||||
}
|
||||
|
||||
LogIndented("Disposing Children");
|
||||
foreach (var child in ChildNodes.ToList()) {
|
||||
child.Dispose();
|
||||
}
|
||||
LogIndented("Children Disposed");
|
||||
ChildNodes.Clear();
|
||||
|
||||
LogIndented("Disposing Tooltip Events");
|
||||
UnregisterTooltipEvents();
|
||||
|
||||
LogIndented("Clearing Native Focus");
|
||||
AtkStage.Instance()->ClearNodeFocus(ResNode);
|
||||
|
||||
LogIndented("Detaching From UI");
|
||||
DetachNode();
|
||||
|
||||
LogIndented("Disposing Timeline");
|
||||
Timeline?.Dispose();
|
||||
ResNode->Timeline = null;
|
||||
|
||||
LogIndented("Invoking Native Dispose");
|
||||
Dispose(true, false);
|
||||
GC.SuppressFinalize(this);
|
||||
CreatedNodes.Remove(this);
|
||||
|
||||
logIndent--;
|
||||
LogIndented("Dispose Complete");
|
||||
logIndent--;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
logIndent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogIndented(string message)
|
||||
=> Log.Verbose(new string(' ', logIndent * 2) + message);
|
||||
|
||||
/// <summary>
|
||||
/// Warning, this is only to ensure there are no memory leaks.
|
||||
/// Ensure you have detached nodes safely from native ui before disposing.
|
||||
/// </summary>
|
||||
internal static void DisposeNodes() {
|
||||
var leakedNodeCount = CreatedNodes.Count(node => !node.IsAddonRootNode && node.ResNode is not null && node.ResNode->ParentNode is null);
|
||||
|
||||
if (leakedNodeCount is not 0) {
|
||||
Log.Warning($"There were {leakedNodeCount} node(s) that were not disposed safely.");
|
||||
}
|
||||
|
||||
foreach (var node in CreatedNodes.ToArray()) {
|
||||
if (node.ResNode is null) continue;
|
||||
if (node.ResNode->ParentNode is not null) continue;
|
||||
if (node.IsAddonRootNode) continue;
|
||||
|
||||
Log.Warning($"Forcing disposal of: {node.GetType()}");
|
||||
node.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
~NodeBase() => Dispose(false, false);
|
||||
|
||||
/// <summary>
|
||||
/// Dispose associated resources. If a resource modifies native state directly guard it with isNativeDestructor
|
||||
/// </summary>
|
||||
/// <param name="disposing">
|
||||
/// Indicates if this specific call should dispose resources or not. This protects against double dispose,
|
||||
/// or incorrectly manipulating native state too many times.
|
||||
/// </param>
|
||||
/// <param name="isNativeDestructor">
|
||||
/// Indicates if the dispose call should try to completely clean up all resources,
|
||||
/// or if it should only clean up managed resources. When false, be sure to only dispose
|
||||
/// resources that exist in managed spaces, as the game has already cleaned up everything else.
|
||||
/// </param>
|
||||
protected virtual void Dispose(bool disposing, bool isNativeDestructor) {
|
||||
|
||||
// Dispose of managed resources that must be disposed regardless of how dispose is invoked
|
||||
DisposeEvents();
|
||||
DisableEditMode(NodeEditMode.Move | NodeEditMode.Resize);
|
||||
}
|
||||
|
||||
private bool IsNodeValid() {
|
||||
if (ResNode is null) return false;
|
||||
if (ResNode->VirtualTable is null) return false;
|
||||
if (ResNode->VirtualTable == AtkEventTarget.StaticVirtualTablePointer) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static implicit operator AtkResNode*(NodeBase node) => node.ResNode;
|
||||
public static implicit operator AtkEventTarget*(NodeBase node) => &node.ResNode->AtkEventTarget;
|
||||
|
||||
protected void BuildVirtualTable() {
|
||||
// Back up original destructor pointer
|
||||
originalDestructorFunction = ResNode->VirtualTable->Destroy;
|
||||
|
||||
// Overwrite virtual table with a custom copy,
|
||||
// Note: Currently there are only 2 vfuncs, but there's no harm in copying more for if they ever add more vfuncs to the game.
|
||||
virtualTable = (AtkResNode.AtkResNodeVirtualTable*)NativeMemoryHelper.Malloc(0x8 * 4);
|
||||
NativeMemory.Copy(ResNode->VirtualTable, virtualTable, 0x8 * 4);
|
||||
ResNode->VirtualTable = virtualTable;
|
||||
|
||||
// Pin managed function to virtual table entry
|
||||
destructorFunction = DestructorDetour;
|
||||
|
||||
// Replace native destructor with
|
||||
virtualTable->Destroy = (delegate* unmanaged<AtkResNode*, bool, void>) Marshal.GetFunctionPointerForDelegate(destructorFunction);
|
||||
}
|
||||
|
||||
private void DestructorDetour(AtkResNode* thisPtr, bool free) {
|
||||
Dispose(true, true);
|
||||
InvokeOriginalDestructor(thisPtr, free);
|
||||
|
||||
Log.Verbose($"Native has disposed node {GetType()}");
|
||||
GC.SuppressFinalize(this);
|
||||
CreatedNodes.Remove(this);
|
||||
|
||||
isDisposed = true;
|
||||
}
|
||||
|
||||
protected void InvokeOriginalDestructor(AtkResNode* thisPtr, bool free) {
|
||||
if (virtualTable is null) return; // Shouldn't be possible, but just in case.
|
||||
|
||||
originalDestructorFunction(thisPtr, free);
|
||||
NativeMemoryHelper.Free(virtualTable, 0x8 * 4);
|
||||
virtualTable = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Dalamud.Game.Addon.Events;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
using KamiToolKit.Nodes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
|
||||
private Vector2 clickStartPosition = Vector2.Zero;
|
||||
private NodeEditMode currentEditMode = 0;
|
||||
|
||||
private ViewportEventListener? editEventListener;
|
||||
|
||||
private bool isCursorSet;
|
||||
|
||||
private bool isMoving;
|
||||
private bool isResizing;
|
||||
|
||||
private NodeEditOverlayNode? overlayNode;
|
||||
|
||||
public Action<NodeBase>? OnResizeComplete { get; set; }
|
||||
public Action<NodeBase>? OnMoveComplete { get; set; }
|
||||
public Action<NodeBase>? OnEditComplete { get; set; }
|
||||
|
||||
public bool EnableMoving {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value) {
|
||||
EnableEditMode(NodeEditMode.Move);
|
||||
}
|
||||
else {
|
||||
DisableEditMode(NodeEditMode.Move);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableResizing {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value) {
|
||||
EnableEditMode(NodeEditMode.Resize);
|
||||
}
|
||||
else {
|
||||
DisableEditMode(NodeEditMode.Resize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void EnableEditMode(NodeEditMode mode) {
|
||||
|
||||
currentEditMode |= mode;
|
||||
|
||||
if (overlayNode is null) {
|
||||
overlayNode = new NodeEditOverlayNode {
|
||||
Position = new Vector2(-16.0f, -16.0f),
|
||||
Size = Size + new Vector2(32.0f, 32.0f),
|
||||
};
|
||||
overlayNode.AttachNode(this);
|
||||
ChildNodes.Add(overlayNode);
|
||||
}
|
||||
|
||||
overlayNode.ShowParts = currentEditMode.HasFlag(NodeEditMode.Resize);
|
||||
|
||||
if (editEventListener is null) {
|
||||
editEventListener = new ViewportEventListener(OnEditEvent);
|
||||
editEventListener.AddEvent(AtkEventType.MouseMove, overlayNode);
|
||||
editEventListener.AddEvent(AtkEventType.MouseDown, overlayNode);
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableEditMode(NodeEditMode mode) {
|
||||
|
||||
currentEditMode &= ~mode;
|
||||
|
||||
if (currentEditMode.HasFlag(NodeEditMode.Resize) || currentEditMode.HasFlag(NodeEditMode.Move)) return;
|
||||
|
||||
if (editEventListener is not null) {
|
||||
editEventListener.RemoveEvent(AtkEventType.MouseMove);
|
||||
editEventListener.RemoveEvent(AtkEventType.MouseDown);
|
||||
editEventListener.Dispose();
|
||||
editEventListener = null;
|
||||
}
|
||||
|
||||
if (overlayNode is not null) {
|
||||
ChildNodes.Remove(overlayNode);
|
||||
overlayNode.DetachNode();
|
||||
overlayNode.Dispose();
|
||||
overlayNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEditEvent(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||
if (overlayNode is null) return;
|
||||
if (editEventListener is null) return;
|
||||
|
||||
ref var mouseData = ref atkEventData->MouseData;
|
||||
var mousePosition = new Vector2(mouseData.PosX, mouseData.PosY);
|
||||
var mouseDelta = mousePosition - clickStartPosition;
|
||||
|
||||
switch (eventType) {
|
||||
// Move Logic
|
||||
case AtkEventType.MouseMove when isMoving: {
|
||||
Position += mouseDelta;
|
||||
clickStartPosition = mousePosition;
|
||||
|
||||
atkEvent->SetEventIsHandled(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// Update hover state when not resizing, as we latch that for the behavior
|
||||
case AtkEventType.MouseMove when !isResizing: {
|
||||
overlayNode.UpdateHover(atkEventData);
|
||||
}
|
||||
break;
|
||||
|
||||
// Resize Logic
|
||||
case AtkEventType.MouseMove when isResizing: {
|
||||
Position += overlayNode.GetPositionDelta(mouseDelta);
|
||||
Size += overlayNode.GetSizeDelta(mouseDelta);
|
||||
|
||||
overlayNode.Size = Size + new Vector2(32.0f, 32.0f);
|
||||
|
||||
clickStartPosition = mousePosition;
|
||||
|
||||
atkEvent->SetEventIsHandled(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// Begin Resize Event
|
||||
case AtkEventType.MouseDown when !isResizing && overlayNode.AnyHovered() && currentEditMode.HasFlag(NodeEditMode.Resize): {
|
||||
editEventListener.AddEvent(AtkEventType.MouseUp, overlayNode);
|
||||
|
||||
isResizing = true;
|
||||
clickStartPosition = mousePosition;
|
||||
|
||||
atkEvent->SetEventIsHandled(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// End Resize Event
|
||||
case AtkEventType.MouseUp when isResizing: {
|
||||
OnResizeComplete?.Invoke(this);
|
||||
OnEditComplete?.Invoke(this);
|
||||
|
||||
isResizing = false;
|
||||
editEventListener.RemoveEvent(AtkEventType.MouseUp);
|
||||
}
|
||||
break;
|
||||
|
||||
// Begin Move Event
|
||||
case AtkEventType.MouseDown when !overlayNode.AnyHovered() && overlayNode.CheckCollision(atkEventData) && !isMoving && currentEditMode.HasFlag(NodeEditMode.Move): {
|
||||
editEventListener.AddEvent(AtkEventType.MouseUp, overlayNode);
|
||||
|
||||
isMoving = true;
|
||||
clickStartPosition = mousePosition;
|
||||
|
||||
atkEvent->SetEventIsHandled(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// End Move Event
|
||||
case AtkEventType.MouseUp when isMoving: {
|
||||
OnMoveComplete?.Invoke(this);
|
||||
OnEditComplete?.Invoke(this);
|
||||
|
||||
isMoving = false;
|
||||
editEventListener.RemoveEvent(AtkEventType.MouseUp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (isCursorSet) {
|
||||
ResetCursor();
|
||||
isCursorSet = false;
|
||||
}
|
||||
|
||||
if (currentEditMode.HasFlag(NodeEditMode.Move)) {
|
||||
if (isMoving) {
|
||||
SetCursor(AddonCursorType.Grab);
|
||||
isCursorSet = true;
|
||||
}
|
||||
else if (CheckCollision(atkEventData)) {
|
||||
SetCursor(AddonCursorType.Hand);
|
||||
isCursorSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (overlayNode.AnyHovered() && currentEditMode.HasFlag(NodeEditMode.Resize)) {
|
||||
overlayNode.SetCursor();
|
||||
isCursorSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetCursor(AddonCursorType cursor)
|
||||
=> DalamudInterface.Instance.AddonEventManager.SetCursor(cursor);
|
||||
|
||||
private static void ResetCursor()
|
||||
=> DalamudInterface.Instance.AddonEventManager.ResetCursor();
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
internal class EventHandlerInfo {
|
||||
public AtkEventListener.Delegates.ReceiveEvent? OnReceiveEventDelegate;
|
||||
public Action? OnActionDelegate;
|
||||
}
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
|
||||
private CustomEventListener? nodeEventListener;
|
||||
private readonly Dictionary<AtkEventType, EventHandlerInfo> eventHandlers = [];
|
||||
|
||||
/// <summary>
|
||||
/// When true, mousing over this node will show the finger cursor icon.
|
||||
/// </summary>
|
||||
public bool ShowClickableCursor {
|
||||
get => DrawFlags.HasFlag(DrawFlags.ClickableCursor);
|
||||
set {
|
||||
if (value) {
|
||||
DrawFlags |= DrawFlags.ClickableCursor;
|
||||
}
|
||||
else {
|
||||
DrawFlags &= ~DrawFlags.ClickableCursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When true, mousing over this node will show the text input cursor icon.
|
||||
/// </summary>
|
||||
public bool ShowTextInputCursor {
|
||||
get => DrawFlags.HasFlag(DrawFlags.TextInputCursor);
|
||||
set {
|
||||
if (value) {
|
||||
DrawFlags |= DrawFlags.TextInputCursor;
|
||||
}
|
||||
else {
|
||||
DrawFlags &= ~DrawFlags.TextInputCursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEvent(AtkEventType eventType, Action callback) {
|
||||
nodeEventListener ??= new CustomEventListener(HandleEvents);
|
||||
|
||||
SetNodeEventFlags(eventType);
|
||||
|
||||
if (eventHandlers.TryAdd(eventType, new EventHandlerInfo { OnActionDelegate = callback })) {
|
||||
Log.Verbose($"[{eventType}] Registered for {GetType()} [{(nint)ResNode:X}]");
|
||||
ResNode->AtkEventManager.RegisterEvent(eventType, 0, this, this, nodeEventListener, false);
|
||||
}
|
||||
else {
|
||||
eventHandlers[eventType].OnActionDelegate += callback;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEvent(AtkEventType eventType, AtkEventListener.Delegates.ReceiveEvent callback) {
|
||||
nodeEventListener ??= new CustomEventListener(HandleEvents);
|
||||
|
||||
SetNodeEventFlags(eventType);
|
||||
|
||||
if (eventHandlers.TryAdd(eventType, new EventHandlerInfo { OnReceiveEventDelegate = callback })) {
|
||||
Log.Verbose($"[{eventType}] Registered for {GetType()} [{(nint)ResNode:X}]");
|
||||
ResNode->AtkEventManager.RegisterEvent(eventType, 0, this, this, nodeEventListener, false);
|
||||
}
|
||||
else {
|
||||
eventHandlers[eventType].OnReceiveEventDelegate += callback;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveEvent(AtkEventType eventType) {
|
||||
if (nodeEventListener is null) return;
|
||||
|
||||
if (eventHandlers.Remove(eventType)) {
|
||||
Log.Verbose($"[{eventType}] Unregistered from {GetType()} [{(nint)ResNode:X}]");
|
||||
ResNode->AtkEventManager.UnregisterEvent(eventType, 0, nodeEventListener, false);
|
||||
}
|
||||
|
||||
// If we have removed the last event, free the event listener
|
||||
if (eventHandlers.Keys.Count is 0) {
|
||||
nodeEventListener.Dispose();
|
||||
nodeEventListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveEvent(AtkEventType eventType, Action callback) {
|
||||
if (nodeEventListener is null) return;
|
||||
|
||||
if (eventHandlers.TryGetValue(eventType, out var handler)) {
|
||||
handler.OnActionDelegate -= callback;
|
||||
|
||||
if (handler.OnReceiveEventDelegate is null && handler.OnActionDelegate is null) {
|
||||
RemoveEvent(eventType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveEvent(AtkEventType eventType, AtkEventListener.Delegates.ReceiveEvent callback) {
|
||||
if (nodeEventListener is null) return;
|
||||
|
||||
if (eventHandlers.TryGetValue(eventType, out var handler)) {
|
||||
handler.OnReceiveEventDelegate -= callback;
|
||||
|
||||
if (handler.OnReceiveEventDelegate is null && handler.OnActionDelegate is null) {
|
||||
RemoveEvent(eventType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeEvents() {
|
||||
if (nodeEventListener is not null) {
|
||||
ResNode->AtkEventManager.UnregisterEvent(AtkEventType.UnregisterAll, 0, nodeEventListener, false);
|
||||
}
|
||||
|
||||
eventHandlers.Clear();
|
||||
|
||||
nodeEventListener?.Dispose();
|
||||
nodeEventListener = null;
|
||||
}
|
||||
|
||||
private void HandleEvents(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||
try {
|
||||
if (!IsVisible) return;
|
||||
|
||||
if (eventHandlers.TryGetValue(eventType, out var handler)) {
|
||||
|
||||
foreach (var noArgHandler in Delegate.EnumerateInvocationList(handler.OnActionDelegate)) {
|
||||
try {
|
||||
noArgHandler();
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var argHandler in Delegate.EnumerateInvocationList(handler.OnReceiveEventDelegate)) {
|
||||
try {
|
||||
argHandler(thisPtr, eventType, eventParam, atkEvent, atkEventData);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetNodeEventFlags(AtkEventType eventType) {
|
||||
switch (eventType) {
|
||||
// Hover events need to propagate down to trigger various timelines
|
||||
case AtkEventType.MouseOver:
|
||||
case AtkEventType.MouseOut:
|
||||
case AtkEventType.MouseWheel:
|
||||
AddNodeFlags(NodeFlags.EmitsEvents, NodeFlags.RespondToMouse);
|
||||
break;
|
||||
|
||||
// Any kind of direct interaction should be a blocking event
|
||||
// set HasCollision to prevent events from propagating
|
||||
case AtkEventType.MouseDown:
|
||||
case AtkEventType.MouseUp:
|
||||
case AtkEventType.MouseMove:
|
||||
case AtkEventType.MouseClick:
|
||||
AddNodeFlags(NodeFlags.EmitsEvents, NodeFlags.RespondToMouse, NodeFlags.HasCollision);
|
||||
break;
|
||||
|
||||
// ButtonClick is mostly used as an event that native calls back to, when interacting with buttons
|
||||
// We do not want to re-emit, or block events in this case
|
||||
case AtkEventType.ButtonClick:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
|
||||
internal readonly List<NodeBase> ChildNodes = [];
|
||||
private NodeBase? parentNode;
|
||||
|
||||
internal AtkUldManager* ParentUldManager { get; set; }
|
||||
internal AtkUnitBase* ParentAddon { get; private set; }
|
||||
|
||||
[OverloadResolutionPriority(1)]
|
||||
public void AttachNode(NativeAddon? targetAddon, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformManagedAttach(targetAddon, targetPosition);
|
||||
|
||||
public void AttachNode(AtkUnitBase* targetAddon, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach(targetAddon is not null ? targetAddon->RootNode : null, targetPosition);
|
||||
|
||||
[OverloadResolutionPriority(1)]
|
||||
public void AttachNode(NodeBase? targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformManagedAttach(targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkResNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach(targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkImageNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkTextNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkNineGridNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkCounterNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkCollisionNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkClippingMaskNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkComponentNode* targetNode, NodePosition targetPosition = NodePosition.AfterAllSiblings)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
private void PerformManagedAttach(NativeAddon? targetAddon, NodePosition targetPosition = NodePosition.AsLastChild) {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
if (targetAddon is null) return;
|
||||
|
||||
// Check the Addon's node list to find out what NodeId we should be, and set that before attaching
|
||||
if (NodeId > NodeIdBase) {
|
||||
NodeId = targetAddon.InternalAddon->UldManager.GetMaxNodeId() + 1;
|
||||
}
|
||||
|
||||
PerformNativeAttach(targetAddon.RootNode, targetPosition);
|
||||
|
||||
parentNode = targetAddon.RootNode;
|
||||
parentNode.ChildNodes.Add(this);
|
||||
}
|
||||
|
||||
private void PerformManagedAttach(NodeBase? targetNode, NodePosition targetPosition) {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
if (targetNode is null) return;
|
||||
|
||||
PerformNativeAttach(targetNode, targetPosition);
|
||||
|
||||
parentNode = targetNode;
|
||||
parentNode.ChildNodes.Add(this);
|
||||
}
|
||||
|
||||
private void PerformNativeAttach(AtkResNode* targetNode, NodePosition targetPosition) {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
if (targetNode is null) return;
|
||||
|
||||
if (targetNode->GetNodeType() is NodeType.Component) {
|
||||
|
||||
// If target is a ComponentNode,
|
||||
// then we don't ever wanna be a child of the ComponentNode itself,
|
||||
// we will want to be a sibling of the root node.
|
||||
// Therefore, redirect the target position to be siblings.
|
||||
targetPosition = targetPosition switch {
|
||||
NodePosition.AsLastChild => NodePosition.AfterAllSiblings,
|
||||
NodePosition.AsFirstChild => NodePosition.BeforeAllSiblings,
|
||||
_ => targetPosition,
|
||||
};
|
||||
|
||||
// If however, we are using BeforeTarget or AfterTarget,
|
||||
// then we do want to attach to the ComponentNode
|
||||
// else, attach to its root node.
|
||||
var componentNode = targetNode->GetAsAtkComponentNode();
|
||||
if (componentNode is not null) {
|
||||
targetNode = targetPosition switch {
|
||||
NodePosition.AfterTarget => targetNode,
|
||||
NodePosition.BeforeTarget => targetNode,
|
||||
NodePosition.AfterAllSiblings => componentNode->Component->UldManager.RootNode,
|
||||
NodePosition.BeforeAllSiblings => componentNode->Component->UldManager.RootNode,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(targetPosition), targetPosition, null),
|
||||
};
|
||||
|
||||
// We also need to check the components node list, to get a safely assigned nodeId
|
||||
if (NodeId > NodeIdBase) {
|
||||
NodeId = componentNode->Component->UldManager.GetMaxNodeId() + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeLinker.AttachNode(this, targetNode, targetPosition);
|
||||
UpdateParentAddon(targetNode);
|
||||
UpdateNative();
|
||||
}
|
||||
|
||||
internal void ReattachNode(AtkResNode* newTarget) {
|
||||
if (newTarget is null) return;
|
||||
|
||||
DetachNode();
|
||||
AttachNode(newTarget);
|
||||
}
|
||||
|
||||
public void DetachNode() {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
if (ResNode is null) return;
|
||||
|
||||
UnlinkFromNative();
|
||||
RemoveUldManagerObjectReferences();
|
||||
RemoveParentAddonReferences();
|
||||
RemoveParentNodeReferences();
|
||||
}
|
||||
|
||||
private void UnlinkFromNative() {
|
||||
NodeLinker.DetachNode(ResNode);
|
||||
ResNode->ParentNode = null;
|
||||
ResNode->NextSiblingNode = null;
|
||||
ResNode->PrevSiblingNode = null;
|
||||
}
|
||||
|
||||
private void RemoveUldManagerObjectReferences() {
|
||||
if (ParentUldManager is null) return;
|
||||
|
||||
ParentUldManager->RemoveNodeFromObjectList(this);
|
||||
ParentUldManager = null;
|
||||
}
|
||||
|
||||
private void RemoveParentAddonReferences() {
|
||||
if (ParentAddon is null) return;
|
||||
|
||||
ParentAddon->UldManager.UpdateDrawNodeList();
|
||||
ParentAddon->UpdateCollisionNodeList(false);
|
||||
|
||||
ParentAddon = null;
|
||||
|
||||
foreach (var child in GetAllChildren(this)) {
|
||||
child.ParentAddon = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveParentNodeReferences() {
|
||||
if (parentNode is null) return;
|
||||
|
||||
parentNode.ChildNodes.Remove(this);
|
||||
parentNode = null;
|
||||
}
|
||||
|
||||
private void UpdateNative() {
|
||||
if (ResNode is null) return;
|
||||
|
||||
MarkDirty();
|
||||
|
||||
if (ParentUldManager is null) {
|
||||
ParentUldManager = GetUldManagerForNode(ResNode);
|
||||
}
|
||||
|
||||
if (ParentUldManager is not null) {
|
||||
ParentUldManager->AddNodeToObjectList(this);
|
||||
}
|
||||
|
||||
if (ParentAddon is not null) {
|
||||
if (ParentAddon->NameString is "NamePlate") {
|
||||
Log.Warning("Warning, attaching to AddonNamePlate is not supported. Use OverlayController instead.");
|
||||
}
|
||||
|
||||
ParentAddon->UldManager.UpdateDrawNodeList();
|
||||
ParentAddon->UpdateCollisionNodeList(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateParentAddon(AtkResNode* node) {
|
||||
if (parentNode is not null && parentNode.ParentAddon is not null) {
|
||||
ParentAddon = parentNode.ParentAddon;
|
||||
}
|
||||
else if (ParentAddon is null) {
|
||||
var targetParentAddon = RaptureAtkUnitManager.Instance()->GetAddonByNode(node);
|
||||
if (targetParentAddon is not null) {
|
||||
ParentAddon = targetParentAddon;
|
||||
}
|
||||
}
|
||||
|
||||
if (ParentAddon is not null) {
|
||||
foreach (var child in GetAllChildren(this)) {
|
||||
child.ParentAddon = ParentAddon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AtkUldManager* GetUldManagerForNode(AtkResNode* node) {
|
||||
if (node is null) return null;
|
||||
|
||||
var targetNode = node;
|
||||
|
||||
if (targetNode->GetNodeType() is NodeType.Component) {
|
||||
targetNode = targetNode->ParentNode;
|
||||
}
|
||||
|
||||
// Try to get UldManager via the first parent that is a component
|
||||
while (targetNode is not null) {
|
||||
if (targetNode->GetNodeType() is NodeType.Component) {
|
||||
var componentNode = (AtkComponentNode*)targetNode;
|
||||
return &componentNode->Component->UldManager;
|
||||
}
|
||||
|
||||
targetNode = targetNode->ParentNode;
|
||||
}
|
||||
|
||||
// We failed to find a parent component, try to get a parent addon instead
|
||||
if (ParentAddon is not null) {
|
||||
return &ParentAddon->UldManager;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<NodeBase> GetAllChildren(NodeBase parent) {
|
||||
foreach (var child in parent.ChildNodes) {
|
||||
yield return child;
|
||||
foreach (var childNode in GetAllChildren(child)) {
|
||||
yield return childNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerable<NodeBase> GetLocalChildren(NodeBase parent) {
|
||||
if (parent is ComponentNode) yield break;
|
||||
|
||||
foreach (var child in parent.ChildNodes) {
|
||||
yield return child;
|
||||
|
||||
if (child is ComponentNode) continue;
|
||||
foreach (var childNode in GetLocalChildren(child)) {
|
||||
yield return childNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
using System;
|
||||
using Dalamud.Interface;
|
||||
using FFXIVClientStructs.FFXIV.Common.Math;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Enums;
|
||||
using Bounds = KamiToolKit.Classes.Bounds;
|
||||
using Vector2 = System.Numerics.Vector2;
|
||||
using Vector3 = System.Numerics.Vector3;
|
||||
using Vector4 = System.Numerics.Vector4;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
public virtual float X {
|
||||
get => ResNode->GetXFloat();
|
||||
set => ResNode->SetXFloat(value);
|
||||
}
|
||||
|
||||
public virtual float Y {
|
||||
get => ResNode->GetYFloat();
|
||||
set => ResNode->SetYFloat(value);
|
||||
}
|
||||
|
||||
public virtual Vector2 Position {
|
||||
get => ResNode->Position;
|
||||
set => ResNode->Position = value;
|
||||
}
|
||||
|
||||
public virtual float ScreenX {
|
||||
get => ResNode->ScreenX;
|
||||
set => ResNode->ScreenX = value;
|
||||
}
|
||||
|
||||
public virtual float ScreenY {
|
||||
get => ResNode->ScreenY;
|
||||
set => ResNode->ScreenY = value;
|
||||
}
|
||||
|
||||
public virtual Vector2 ScreenPosition
|
||||
=> ResNode->ScreenPosition;
|
||||
|
||||
public virtual float Width {
|
||||
get => ResNode->GetWidth();
|
||||
set {
|
||||
ResNode->SetWidth((ushort)value);
|
||||
OnSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual float Height {
|
||||
get => ResNode->GetHeight();
|
||||
set {
|
||||
ResNode->SetHeight((ushort)value);
|
||||
OnSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Vector2 Size {
|
||||
get => ResNode->Size;
|
||||
set {
|
||||
ResNode->SetWidth((ushort)value.X);
|
||||
ResNode->SetHeight((ushort)value.Y);
|
||||
OnSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public Bounds Bounds
|
||||
=> ResNode->Bounds;
|
||||
|
||||
public Vector2 Center
|
||||
=> ResNode->Center;
|
||||
|
||||
public virtual float ScaleX {
|
||||
get => ResNode->GetScaleX();
|
||||
set => ResNode->SetScaleX(value);
|
||||
}
|
||||
|
||||
public virtual float ScaleY {
|
||||
get => ResNode->GetScaleY();
|
||||
set => ResNode->SetScaleY(value);
|
||||
}
|
||||
|
||||
public virtual Vector2 Scale {
|
||||
get => ResNode->Scale;
|
||||
set => ResNode->Scale = value;
|
||||
}
|
||||
|
||||
public virtual float Rotation {
|
||||
get => ResNode->GetRotation();
|
||||
set => ResNode->SetRotation(value);
|
||||
}
|
||||
|
||||
public virtual float RotationDegrees {
|
||||
get => ResNode->RotationDegrees;
|
||||
set => ResNode->RotationDegrees = value;
|
||||
}
|
||||
|
||||
public virtual float OriginX {
|
||||
get => ResNode->OriginX;
|
||||
set => ResNode->OriginX = value;
|
||||
}
|
||||
|
||||
public virtual float OriginY {
|
||||
get => ResNode->OriginY;
|
||||
set => ResNode->OriginY = value;
|
||||
}
|
||||
|
||||
public virtual Vector2 Origin {
|
||||
get => ResNode->Origin;
|
||||
set => ResNode->Origin = value;
|
||||
}
|
||||
|
||||
private bool? lastIsVisible;
|
||||
|
||||
public virtual bool IsVisible {
|
||||
get => ResNode->Visible;
|
||||
set {
|
||||
ResNode->Visible = value;
|
||||
if (lastIsVisible is null || lastIsVisible != value) {
|
||||
OnVisibilityToggled?.Invoke(value);
|
||||
lastIsVisible = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Action<bool>? OnVisibilityToggled { get; set; }
|
||||
|
||||
public NodeFlags NodeFlags {
|
||||
get => ResNode->NodeFlags;
|
||||
set => ResNode->NodeFlags = value;
|
||||
}
|
||||
|
||||
public virtual Vector4 Color {
|
||||
get => ResNode->ColorVector;
|
||||
set => ResNode->ColorVector = value;
|
||||
}
|
||||
|
||||
public virtual ColorHelpers.HsvaColor ColorHsva {
|
||||
get => ResNode->ColorHsva;
|
||||
set => ResNode->ColorHsva = value;
|
||||
}
|
||||
|
||||
public virtual float Alpha {
|
||||
get => ResNode->Color.A;
|
||||
set => ResNode->SetAlpha((byte)(value * 255.0f));
|
||||
}
|
||||
|
||||
public virtual Vector3 AddColor {
|
||||
get => ResNode->AddColor;
|
||||
set => ResNode->AddColor = value;
|
||||
}
|
||||
|
||||
public virtual ColorHelpers.HsvaColor AddColorHsva {
|
||||
get => ResNode->AddColorHsva;
|
||||
set => ResNode->AddColorHsva = value;
|
||||
}
|
||||
|
||||
public virtual Vector3 MultiplyColor {
|
||||
get => ResNode->MultiplyColor;
|
||||
set => ResNode->MultiplyColor = value;
|
||||
}
|
||||
|
||||
public virtual ColorHelpers.HsvaColor MultiplyColorHsva {
|
||||
get => ResNode->MultiplyColorHsva;
|
||||
set => ResNode->MultiplyColorHsva = value;
|
||||
}
|
||||
|
||||
public uint NodeId {
|
||||
get => ResNode->NodeId;
|
||||
set => ResNode->NodeId = value;
|
||||
}
|
||||
|
||||
public virtual DrawFlags DrawFlags {
|
||||
get => (DrawFlags) ResNode->DrawFlags;
|
||||
set => ResNode->DrawFlags = (uint) value & 0b1111_1111_1111_1100_0000_0011_1111_1111 |
|
||||
ResNode->DrawFlags & 0b0000_0000_0000_0011_1111_1100_0000_0000;
|
||||
}
|
||||
|
||||
public virtual int ClipCount {
|
||||
get => (int)((ResNode->DrawFlags & 0b0000_0000_0000_0011_1111_1100_0000_0000) >> 10);
|
||||
set => ResNode->DrawFlags = (uint)(value << 10 & 0b0000_0000_0000_0011_1111_1100_0000_0000)
|
||||
| ResNode->DrawFlags & 0b1111_1111_1111_1100_0000_0011_1111_1111;
|
||||
}
|
||||
|
||||
public void AddDrawFlags(params DrawFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
DrawFlags |= flag;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveDrawFlags(params DrawFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
DrawFlags &= ~flag;
|
||||
}
|
||||
}
|
||||
|
||||
public int Priority {
|
||||
get => ResNode->GetPriority();
|
||||
set => ResNode->SetPriority((ushort)value);
|
||||
}
|
||||
|
||||
protected virtual NodeType NodeType {
|
||||
get => ResNode->GetNodeType();
|
||||
set => ResNode->Type = value;
|
||||
}
|
||||
|
||||
public virtual int ChildCount
|
||||
=> ResNode->ChildCount;
|
||||
|
||||
protected virtual void OnSizeChanged() { }
|
||||
|
||||
public void AddNodeFlags(params NodeFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
NodeFlags |= flag;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveNodeFlags(params NodeFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
NodeFlags &= ~flag;
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkDirty() {
|
||||
foreach (var child in GetAllChildren(this)) {
|
||||
child.ResNode->AddDrawFlag( [ DrawFlags.IsDirty ] );
|
||||
}
|
||||
ResNode->AddDrawFlag([ DrawFlags.IsDirty ] );
|
||||
}
|
||||
|
||||
public bool CheckCollision(short x, short y, bool inclusive = true)
|
||||
=> ResNode->CheckCollision(x, y, inclusive);
|
||||
|
||||
public bool CheckCollision(Vector2 position, bool inclusive = true)
|
||||
=> ResNode->CheckCollision((short) position.X, (short) position.Y, inclusive);
|
||||
|
||||
public bool CheckCollision(AtkEventData* eventData, bool inclusive = true)
|
||||
=> ResNode->CheckCollision(eventData, inclusive);
|
||||
|
||||
public Matrix2x2 Transform {
|
||||
get => ResNode->Transform;
|
||||
set => ResNode->Transform = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using KamiToolKit.Timelines;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
|
||||
public Timeline? Timeline { get; private set; }
|
||||
|
||||
public void AddTimeline(Timeline timeline) {
|
||||
Timeline?.Dispose();
|
||||
|
||||
Timeline = timeline;
|
||||
ResNode->Timeline = timeline.InternalTimeline;
|
||||
timeline.OwnerNode = ResNode;
|
||||
}
|
||||
|
||||
public void AddTimeline(TimelineBuilder builder)
|
||||
=> AddTimeline(builder.Build());
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Enums;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Nodes;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public record InventoryItemTooltip(InventoryType Inventory, short Slot);
|
||||
|
||||
public unsafe partial class NodeBase {
|
||||
|
||||
private AtkTooltipManager.AtkTooltipType tooltipType = AtkTooltipManager.AtkTooltipType.None;
|
||||
private bool tooltipEventsRegistered;
|
||||
|
||||
public virtual ReadOnlySeString TextTooltip {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (!value.IsEmpty) {
|
||||
TryRegisterTooltipEvents();
|
||||
tooltipType |= AtkTooltipManager.AtkTooltipType.Text;
|
||||
}
|
||||
else {
|
||||
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual uint ActionTooltip {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value is not 0) {
|
||||
TryRegisterTooltipEvents();
|
||||
tooltipType |= AtkTooltipManager.AtkTooltipType.Action;
|
||||
}
|
||||
else {
|
||||
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual uint ItemTooltip {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value is not 0) {
|
||||
TryRegisterTooltipEvents();
|
||||
tooltipType |= AtkTooltipManager.AtkTooltipType.Item;
|
||||
}
|
||||
else {
|
||||
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual InventoryItemTooltip? InventoryItemTooltip {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value is not null) {
|
||||
TryRegisterTooltipEvents();
|
||||
tooltipType |= AtkTooltipManager.AtkTooltipType.Item;
|
||||
}
|
||||
else {
|
||||
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TryRegisterTooltipEvents() {
|
||||
if (tooltipEventsRegistered) return;
|
||||
|
||||
AddEvent(AtkEventType.MouseOver, ShowTooltip);
|
||||
AddEvent(AtkEventType.MouseOut, HideTooltip);
|
||||
OnVisibilityToggled += ToggleCollisionFlag;
|
||||
ToggleCollisionFlag(IsVisible);
|
||||
|
||||
tooltipEventsRegistered = true;
|
||||
}
|
||||
|
||||
private void UnregisterTooltipEvents() {
|
||||
if (tooltipEventsRegistered) {
|
||||
RemoveEvent(AtkEventType.MouseOver, ShowTooltip);
|
||||
RemoveEvent(AtkEventType.MouseOut, HideTooltip);
|
||||
OnVisibilityToggled -= ToggleCollisionFlag;
|
||||
tooltipEventsRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleCollisionFlag(bool isVisible) {
|
||||
if (this is ComponentNode) return;
|
||||
|
||||
if (isVisible) {
|
||||
AddNodeFlags(NodeFlags.HasCollision);
|
||||
}
|
||||
else {
|
||||
RemoveNodeFlags(NodeFlags.HasCollision);
|
||||
}
|
||||
}
|
||||
|
||||
protected bool TooltipRegistered { get; set; }
|
||||
|
||||
public void ShowTooltip() {
|
||||
if (ParentAddon is null) return; // Shouldn't be possible
|
||||
if (tooltipType is AtkTooltipManager.AtkTooltipType.None) return;
|
||||
|
||||
using var stringBuilder = new RentedSeStringBuilder();
|
||||
using var stringBuffer = new AtkValue();
|
||||
if (!TextTooltip.IsEmpty) {
|
||||
stringBuffer.SetManagedString(stringBuilder.Builder.Append(TextTooltip).GetViewAsSpan());
|
||||
}
|
||||
|
||||
var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs();
|
||||
|
||||
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Text)) {
|
||||
tooltipArgs.TextArgs.AtkArrayType = 0;
|
||||
tooltipArgs.TextArgs.Text = stringBuffer.String;
|
||||
}
|
||||
|
||||
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Action)) {
|
||||
tooltipArgs.ActionArgs.Flags = 1;
|
||||
tooltipArgs.ActionArgs.Kind = DetailKind.Action;
|
||||
tooltipArgs.ActionArgs.Id = (int)ActionTooltip;
|
||||
}
|
||||
|
||||
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Item) && InventoryItemTooltip is {} inventoryTooltip) {
|
||||
tooltipArgs.ItemArgs.Kind = DetailKind.InventoryItem;
|
||||
tooltipArgs.ItemArgs.InventoryType = inventoryTooltip.Inventory;
|
||||
tooltipArgs.ItemArgs.Slot = inventoryTooltip.Slot;
|
||||
tooltipArgs.ItemArgs.BuyQuantity = -1;
|
||||
tooltipArgs.ItemArgs.Flag1 = 0;
|
||||
}
|
||||
else if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Item) && InventoryItemTooltip is null) {
|
||||
tooltipArgs.ItemArgs.Kind = DetailKind.Item;
|
||||
tooltipArgs.ItemArgs.ItemId = (int) ItemTooltip;
|
||||
tooltipArgs.ItemArgs.BuyQuantity = -1;
|
||||
tooltipArgs.ItemArgs.Flag1 = 0;
|
||||
}
|
||||
|
||||
AtkStage.Instance()->TooltipManager.ShowTooltip(tooltipType, ParentAddon->Id, this, &tooltipArgs);
|
||||
}
|
||||
|
||||
public void HideTooltip() {
|
||||
if (ParentAddon is null) return;
|
||||
|
||||
AtkStage.Instance()->TooltipManager.HideTooltip(ParentAddon->Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe class NodeBase<T> : NodeBase where T : unmanaged, ICreatable {
|
||||
protected NodeBase(NodeType nodeType) {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
|
||||
Log.Verbose($"Creating new node {GetType()}");
|
||||
Node = NativeMemoryHelper.Create<T>();
|
||||
|
||||
if (ResNode is null) {
|
||||
throw new Exception($"Unable to allocate memory for {typeof(T)}");
|
||||
}
|
||||
|
||||
KamiToolKitLibrary.AllocatedNodes?.TryAdd((nint)Node, GetType());
|
||||
|
||||
BuildVirtualTable();
|
||||
|
||||
ResNode->Type = nodeType;
|
||||
ResNode->NodeId = NodeIdBase + CurrentOffset++;
|
||||
ResNode->ToggleVisibility(true);
|
||||
|
||||
CreatedNodes.Add(this);
|
||||
}
|
||||
|
||||
public T* Node { get; private set; }
|
||||
|
||||
internal sealed override AtkResNode* ResNode => (AtkResNode*)Node;
|
||||
|
||||
public static implicit operator T*(NodeBase<T> node) => (T*) node.ResNode;
|
||||
|
||||
protected override void Dispose(bool disposing, bool isNativeDestructor) {
|
||||
if (disposing) {
|
||||
try {
|
||||
base.Dispose(disposing, isNativeDestructor);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
finally {
|
||||
if (!isNativeDestructor) {
|
||||
InvokeOriginalDestructor(ResNode, true);
|
||||
}
|
||||
|
||||
KamiToolKitLibrary.AllocatedNodes?.Remove((nint)Node, out _);
|
||||
|
||||
Node = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user