#if INPUTSYSTEM_SUPPORT using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using AlicizaX.UI.Runtime; using UnityEngine; using UnityEngine.InputSystem; namespace UnityEngine.UI { internal enum EHotkeyPressType : byte { Started = 0, Performed = 1 } internal readonly struct HotkeyRegistration { public readonly IHotkeyTrigger Trigger; public readonly EHotkeyPressType PressType; [MethodImpl(MethodImplOptions.AggressiveInlining)] public HotkeyRegistration(IHotkeyTrigger trigger, EHotkeyPressType pressType) { Trigger = trigger; PressType = pressType; } } internal sealed class HotkeyScope { public HotkeyScope(UIHolderObjectBase holder) { Holder = holder; HierarchyDepth = GetHierarchyDepth(holder.transform); BlocksLowerScopes = FindParentHolder(holder) == null; } public readonly UIHolderObjectBase Holder; public readonly int HierarchyDepth; public readonly bool BlocksLowerScopes; public readonly Dictionary> RegistrationsByAction = new(StringComparer.Ordinal); public bool LifecycleActive; public ulong ActivationSerial; public Action OnBeforeShow; public Action OnBeforeClosed; public Action OnDestroy; private Canvas _canvas; public Canvas Canvas { get { if (_canvas == null && Holder != null) { _canvas = Holder.GetComponent(); } return _canvas; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetHierarchyDepth(Transform current) { int depth = 0; while (current != null) { depth++; current = current.parent; } return depth; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static UIHolderObjectBase FindParentHolder(UIHolderObjectBase holder) { if (holder == null) { return null; } Transform current = holder.transform.parent; while (current != null) { if (current.TryGetComponent(out var parentHolder)) { return parentHolder; } current = current.parent; } return null; } } internal sealed class ActionRegistrationBucket { public InputActionReference ActionReference; public Action StartedHandler; public Action PerformedHandler; public int StartedCount; public int PerformedCount; public int TotalCount => StartedCount + PerformedCount; } internal readonly struct TriggerRegistration { public readonly string ActionId; public readonly UIHolderObjectBase Holder; public readonly EHotkeyPressType PressType; public TriggerRegistration(string actionId, UIHolderObjectBase holder, EHotkeyPressType pressType) { ActionId = actionId; Holder = holder; PressType = pressType; } } internal static class UXHotkeyRegisterManager { private static readonly Dictionary _actions = new(StringComparer.Ordinal); private static readonly Dictionary _triggerMap = new(); private static readonly Dictionary _scopes = new(); private static readonly List _leafScopes = new(); private static readonly HashSet _ancestorHolders = new(); private static ulong _serialCounter; #if UNITY_EDITOR [UnityEditor.Callbacks.DidReloadScripts] internal static void ClearHotkeyRegistry() { IHotkeyTrigger[] triggers = new IHotkeyTrigger[_triggerMap.Count]; int index = 0; foreach (var kvp in _triggerMap) { triggers[index++] = kvp.Key; } for (int i = 0; i < triggers.Length; i++) { UnregisterHotkey(triggers[i]); } _actions.Clear(); _triggerMap.Clear(); _scopes.Clear(); _leafScopes.Clear(); _ancestorHolders.Clear(); _serialCounter = 0; } #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void RegisterHotkey(IHotkeyTrigger trigger, UIHolderObjectBase holder, InputActionReference action, EHotkeyPressType pressType) { if (trigger == null || holder == null || action == null || action.action == null) { return; } UnregisterHotkey(trigger); string actionId = action.action.id.ToString(); HotkeyScope scope = GetOrCreateScope(holder); ActionRegistrationBucket bucket = GetOrCreateBucket(actionId, action); HotkeyRegistration registration = new HotkeyRegistration(trigger, pressType); AdjustBucketSubscription(bucket, pressType, true); AddScopeRegistration(scope, actionId, registration); if (scope.LifecycleActive) { scope.ActivationSerial = ++_serialCounter; } _triggerMap[trigger] = new TriggerRegistration(actionId, holder, pressType); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void UnregisterHotkey(IHotkeyTrigger trigger) { if (trigger == null || !_triggerMap.TryGetValue(trigger, out var triggerRegistration)) { return; } if (_actions.TryGetValue(triggerRegistration.ActionId, out var bucket)) { RemoveActionRegistration(bucket, triggerRegistration.PressType, triggerRegistration.ActionId); } if (_scopes.TryGetValue(triggerRegistration.Holder, out var scope)) { RemoveScopeRegistration(scope, triggerRegistration.ActionId, trigger); ReleaseScopeIfEmpty(scope); } _triggerMap.Remove(trigger); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ActionRegistrationBucket GetOrCreateBucket(string actionId, InputActionReference action) { if (_actions.TryGetValue(actionId, out var bucket)) { return bucket; } bucket = new ActionRegistrationBucket { ActionReference = action, StartedHandler = _ => Dispatch(actionId, EHotkeyPressType.Started), PerformedHandler = _ => Dispatch(actionId, EHotkeyPressType.Performed) }; _actions[actionId] = bucket; return bucket; } private static HotkeyScope GetOrCreateScope(UIHolderObjectBase holder) { if (_scopes.TryGetValue(holder, out var scope)) { return scope; } scope = new HotkeyScope(holder) { LifecycleActive = IsHolderVisible(holder), ActivationSerial = ++_serialCounter }; scope.OnBeforeShow = () => ActivateScope(holder); scope.OnBeforeClosed = () => DeactivateScope(holder); scope.OnDestroy = () => DestroyScope(holder); holder.OnWindowBeforeShowEvent += scope.OnBeforeShow; holder.OnWindowBeforeClosedEvent += scope.OnBeforeClosed; holder.OnWindowDestroyEvent += scope.OnDestroy; _scopes[holder] = scope; return scope; } private static void ActivateScope(UIHolderObjectBase holder) { if (_scopes.TryGetValue(holder, out var scope)) { scope.LifecycleActive = true; scope.ActivationSerial = ++_serialCounter; } } private static void DeactivateScope(UIHolderObjectBase holder) { if (_scopes.TryGetValue(holder, out var scope)) { scope.LifecycleActive = false; } } private static void DestroyScope(UIHolderObjectBase holder) { if (holder == null || !_scopes.TryGetValue(holder, out var scope)) { return; } List triggers = null; foreach (var pair in scope.RegistrationsByAction) { List registrations = pair.Value; for (int i = 0; i < registrations.Count; i++) { triggers ??= new List(registrations.Count); triggers.Add(registrations[i].Trigger); } } if (triggers != null) { for (int i = 0; i < triggers.Count; i++) { UnregisterHotkey(triggers[i]); } } DetachScope(scope); } private static void DetachScope(HotkeyScope scope) { if (scope == null || scope.Holder == null) { return; } scope.Holder.OnWindowBeforeShowEvent -= scope.OnBeforeShow; scope.Holder.OnWindowBeforeClosedEvent -= scope.OnBeforeClosed; scope.Holder.OnWindowDestroyEvent -= scope.OnDestroy; _scopes.Remove(scope.Holder); } private static void ReleaseScopeIfEmpty(HotkeyScope scope) { if (scope != null && scope.RegistrationsByAction.Count == 0) { DetachScope(scope); } } private static void AddScopeRegistration(HotkeyScope scope, string actionId, HotkeyRegistration registration) { if (!scope.RegistrationsByAction.TryGetValue(actionId, out var registrations)) { registrations = new List(); scope.RegistrationsByAction[actionId] = registrations; } registrations.Add(registration); } private static void RemoveScopeRegistration(HotkeyScope scope, string actionId, IHotkeyTrigger trigger) { if (!scope.RegistrationsByAction.TryGetValue(actionId, out var registrations)) { return; } for (int i = registrations.Count - 1; i >= 0; i--) { if (ReferenceEquals(registrations[i].Trigger, trigger)) { registrations.RemoveAt(i); break; } } if (registrations.Count == 0) { scope.RegistrationsByAction.Remove(actionId); } } private static void RemoveActionRegistration(ActionRegistrationBucket bucket, EHotkeyPressType pressType, string actionId) { AdjustBucketSubscription(bucket, pressType, false); if (bucket.TotalCount == 0) { _actions.Remove(actionId); } } private static void AdjustBucketSubscription(ActionRegistrationBucket bucket, EHotkeyPressType pressType, bool add) { InputAction inputAction = bucket.ActionReference != null ? bucket.ActionReference.action : null; if (inputAction == null) { return; } switch (pressType) { case EHotkeyPressType.Started: if (add) { if (bucket.StartedCount == 0) { inputAction.started += bucket.StartedHandler; } bucket.StartedCount++; } else if (bucket.StartedCount > 0) { bucket.StartedCount--; if (bucket.StartedCount == 0) { inputAction.started -= bucket.StartedHandler; } } break; case EHotkeyPressType.Performed: if (add) { if (bucket.PerformedCount == 0) { inputAction.performed += bucket.PerformedHandler; } bucket.PerformedCount++; } else if (bucket.PerformedCount > 0) { bucket.PerformedCount--; if (bucket.PerformedCount == 0) { inputAction.performed -= bucket.PerformedHandler; } } break; } if (bucket.TotalCount > 0) { inputAction.Enable(); } else { inputAction.Disable(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Dispatch(string actionId, EHotkeyPressType pressType) { HotkeyScope[] leafScopes = GetLeafScopes(); if (leafScopes.Length == 0) { return; } TryDispatchToScopeChain(leafScopes[0], actionId, pressType); } private static bool TryDispatchToScopeChain(HotkeyScope leafScope, string actionId, EHotkeyPressType pressType) { UIHolderObjectBase currentHolder = leafScope.Holder; while (currentHolder != null) { if (_scopes.TryGetValue(currentHolder, out var scope) && TryGetLatestRegistration(scope, actionId, pressType, out var registration)) { registration.Trigger?.HotkeyActionTrigger(); return true; } currentHolder = FindParentHolder(currentHolder); } return false; } private static bool TryGetLatestRegistration(HotkeyScope scope, string actionId, EHotkeyPressType pressType, out HotkeyRegistration registration) { if (scope.RegistrationsByAction.TryGetValue(actionId, out var registrations)) { for (int i = registrations.Count - 1; i >= 0; i--) { HotkeyRegistration candidate = registrations[i]; if (candidate.PressType == pressType && candidate.Trigger != null) { registration = candidate; return true; } } } registration = default; return false; } private static HotkeyScope[] GetLeafScopes() { _leafScopes.Clear(); _ancestorHolders.Clear(); foreach (var scope in _scopes.Values) { if (!IsScopeActive(scope)) { continue; } UIHolderObjectBase parentHolder = FindParentHolder(scope.Holder); while (parentHolder != null) { _ancestorHolders.Add(parentHolder); parentHolder = FindParentHolder(parentHolder); } } foreach (var scope in _scopes.Values) { if (IsScopeActive(scope) && !_ancestorHolders.Contains(scope.Holder)) { _leafScopes.Add(scope); } } _leafScopes.Sort(CompareScopePriority); return _leafScopes.ToArray(); } private static bool IsScopeActive(HotkeyScope scope) { if (scope == null || !scope.LifecycleActive) { return false; } UIHolderObjectBase holder = scope.Holder; if (holder == null || !holder.IsValid()) { return false; } if (!holder.gameObject.activeInHierarchy) { return false; } Canvas canvas = scope.Canvas; return canvas != null && canvas.gameObject.layer == UIComponent.UIShowLayer; } private static bool IsHolderVisible(UIHolderObjectBase holder) { if (holder == null || !holder.gameObject.activeInHierarchy) { return false; } Canvas canvas = holder.GetComponent(); return canvas != null && canvas.gameObject.layer == UIComponent.UIShowLayer; } private static int CompareScopePriority(HotkeyScope left, HotkeyScope right) { int leftDepth = left.Canvas != null ? left.Canvas.sortingOrder : int.MinValue; int rightDepth = right.Canvas != null ? right.Canvas.sortingOrder : int.MinValue; int depthCompare = rightDepth.CompareTo(leftDepth); if (depthCompare != 0) { return depthCompare; } int hierarchyCompare = right.HierarchyDepth.CompareTo(left.HierarchyDepth); if (hierarchyCompare != 0) { return hierarchyCompare; } return right.ActivationSerial.CompareTo(left.ActivationSerial); } private static UIHolderObjectBase FindParentHolder(UIHolderObjectBase holder) { if (holder == null) { return null; } Transform current = holder.transform.parent; while (current != null) { if (current.TryGetComponent(out var parentHolder)) { return parentHolder; } current = current.parent; } return null; } #if UNITY_EDITOR public static string GetDebugInfo() { return $"Actions: {_actions.Count}, Triggers: {_triggerMap.Count}, Scopes: {_scopes.Count}"; } #endif } } namespace UnityEngine.UI { public static class UXHotkeyHotkeyExtension { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void BindHotKey(this IHotkeyTrigger trigger) { if (trigger?.HotkeyAction == null) { return; } if (trigger is not Component component) { return; } UIHolderObjectBase holder = component.GetComponentInParent(true); if (holder == null) { Debug.LogWarning($"{nameof(HotkeyComponent)} could not find a {nameof(UIHolderObjectBase)} owner.", component); return; } UXHotkeyRegisterManager.RegisterHotkey(trigger, holder, trigger.HotkeyAction, trigger.HotkeyPressType); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnBindHotKey(this IHotkeyTrigger trigger) { if (trigger?.HotkeyAction != null) { UXHotkeyRegisterManager.UnregisterHotkey(trigger); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void BindHotKeyBatch(this IHotkeyTrigger[] triggers) { if (triggers == null) { return; } for (int i = 0; i < triggers.Length; i++) { triggers[i]?.BindHotKey(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnBindHotKeyBatch(this IHotkeyTrigger[] triggers) { if (triggers == null) { return; } for (int i = 0; i < triggers.Length; i++) { triggers[i]?.UnBindHotKey(); } } } } #endif