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 eventHandlers = []; /// /// When true, mousing over this node will show the finger cursor icon. /// public bool ShowClickableCursor { get => DrawFlags.HasFlag(DrawFlags.ClickableCursor); set { if (value) { DrawFlags |= DrawFlags.ClickableCursor; } else { DrawFlags &= ~DrawFlags.ClickableCursor; } } } /// /// When true, mousing over this node will show the text input cursor icon. /// 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; } } }