From 4364f4673d24c4a27062c46526ca5f6687675f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Fri, 20 Mar 2026 13:11:41 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=82=E9=85=8DUI=E6=A1=86=E6=9E=B6=E7=9A=84?= =?UTF-8?q?=E7=83=AD=E9=94=AE=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UX/Hotkey/HotkeyBindComponentInspector.cs | 99 --- .../HotkeyBindComponentInspector.cs.meta | 3 - Editor/UX/Hotkey/HotkeyComponentEditor.cs | 24 +- .../UXComponent/Hotkey/HotkeyBindComponent.cs | 79 -- .../Hotkey/HotkeyBindComponent.cs.meta | 3 - Runtime/UXComponent/Hotkey/HotkeyComponent.cs | 65 +- .../Hotkey/UXHotkeyRegisterManager.cs | 773 ++++++++++++------ 7 files changed, 585 insertions(+), 461 deletions(-) delete mode 100644 Editor/UX/Hotkey/HotkeyBindComponentInspector.cs delete mode 100644 Editor/UX/Hotkey/HotkeyBindComponentInspector.cs.meta delete mode 100644 Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs delete mode 100644 Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs.meta diff --git a/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs b/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs deleted file mode 100644 index 4969b32..0000000 --- a/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs +++ /dev/null @@ -1,99 +0,0 @@ -#if INPUTSYSTEM_SUPPORT -using System.Linq; -using System.Reflection; -using AlicizaX.UI.Runtime; -using UnityEngine; -using UnityEngine.UI; - -namespace UnityEditor.UI -{ - [CustomEditor(typeof(HotkeyBindComponent))] - public class HotkeyBindComponentInspector : UnityEditor.Editor - { - private SerializedProperty hotButtonsProp; - private HotkeyBindComponent _target; - - private void OnEnable() - { - _target = (HotkeyBindComponent)target; - hotButtonsProp = serializedObject.FindProperty("hotButtons"); - } - - public override void OnInspectorGUI() - { - serializedObject.Update(); - - var holder = _target.GetComponent(); - if (holder == null) - { - EditorGUILayout.HelpBox( - "⚠ 当前对象缺少 UIHolderObjectBase 组件。\nHotkeyBindComponent 依赖它进行热键绑定事件。", - MessageType.Error - ); - } - - EditorGUILayout.Space(); - - // 灰掉显示 hotButtons 列表(不可手动编辑) - GUI.enabled = false; - DrawHotButtonListAlwaysExpanded(hotButtonsProp); - GUI.enabled = true; - - EditorGUILayout.Space(); - - // 按钮:扫描子物体(包含隐藏) - if (GUILayout.Button("🔍 扫描所有子物体 (包含隐藏对象)")) - { - FindAllUXHotkeys(); - } - - serializedObject.ApplyModifiedProperties(); - } - - /// - /// 自定义展开显示 hotButtons 列表 - /// - private void DrawHotButtonListAlwaysExpanded(SerializedProperty listProp) - { - EditorGUILayout.LabelField("Hot Buttons", EditorStyles.boldLabel); - - if (listProp == null || listProp.arraySize == 0) - { - EditorGUILayout.HelpBox("当前没有绑定任何 UXHotkey。", MessageType.Info); - return; - } - - EditorGUI.indentLevel++; - for (int i = 0; i < listProp.arraySize; i++) - { - var element = listProp.GetArrayElementAtIndex(i); - var comp = element.objectReferenceValue as Component; - string name = comp != null ? comp.name + " (" + comp.GetType().Name + ")" : "Null"; - // 注意:这里用 typeof(Component) - EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(Component), true); - } - EditorGUI.indentLevel--; - } - - /// - /// 查找所有子物体(包含隐藏)并绑定 UXHotkey - /// - private void FindAllUXHotkeys() - { - Undo.RecordObject(_target, "Scan UXHotkey"); - - var collectMethod = target.GetType().GetMethod("CollectUXHotkeys", BindingFlags.NonPublic | BindingFlags.Instance); - if (collectMethod != null) - { - collectMethod.Invoke(target, null); - EditorUtility.SetDirty(_target); - serializedObject.Update(); - } - else - { - Debug.LogWarning("未找到 CollectUXHotkeys 方法。"); - } - } - } -} -#endif diff --git a/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs.meta b/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs.meta deleted file mode 100644 index d68aaf1..0000000 --- a/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: f9779e310e684e9497bdac4f4ac3983e -timeCreated: 1760340548 \ No newline at end of file diff --git a/Editor/UX/Hotkey/HotkeyComponentEditor.cs b/Editor/UX/Hotkey/HotkeyComponentEditor.cs index 5adcf6f..ae4ab23 100644 --- a/Editor/UX/Hotkey/HotkeyComponentEditor.cs +++ b/Editor/UX/Hotkey/HotkeyComponentEditor.cs @@ -1,3 +1,4 @@ +using AlicizaX.UI.Runtime; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; @@ -21,10 +22,25 @@ namespace UnityEditor.UI public override void OnInspectorGUI() { serializedObject.Update(); + + HotkeyComponent hotkeyComponent = (HotkeyComponent)target; + + EditorGUILayout.HelpBox( + "Hotkeys auto-register to the nearest UIHolderObjectBase at runtime.", + MessageType.Info + ); + + if (hotkeyComponent.GetComponentInParent(true) == null) + { + EditorGUILayout.HelpBox( + "No UIHolderObjectBase was found in parents. This hotkey will not register at runtime.", + MessageType.Warning + ); + } + if (_component.objectReferenceValue == null) { - EditorGUILayout.HelpBox("物体身上不存在ISubmitHandler得到组件", MessageType.Error); - HotkeyComponent hotkeyComponent = (HotkeyComponent)target; + EditorGUILayout.HelpBox("No submit target was found on this object.", MessageType.Error); if (hotkeyComponent.TryGetComponent(typeof(ISubmitHandler), out Component submitHandler)) { _component.objectReferenceValue = submitHandler; @@ -39,8 +55,8 @@ namespace UnityEditor.UI EditorGUILayout.PropertyField(_component, new GUIContent("Component")); EditorGUI.EndDisabledGroup(); - EditorGUILayout.PropertyField(_hotkeyAction, new GUIContent("输入映射")); - EditorGUILayout.PropertyField(_hotkeyPressType, new GUIContent("触发类型")); + EditorGUILayout.PropertyField(_hotkeyAction, new GUIContent("Input Action")); + EditorGUILayout.PropertyField(_hotkeyPressType, new GUIContent("Press Type")); } serializedObject.ApplyModifiedProperties(); diff --git a/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs b/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs deleted file mode 100644 index 57395de..0000000 --- a/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs +++ /dev/null @@ -1,79 +0,0 @@ -#if INPUTSYSTEM_SUPPORT -using System.Linq; -using AlicizaX.UI.Runtime; -using UnityEngine; - -namespace UnityEngine.UI -{ - public class HotkeyBindComponent : MonoBehaviour - { - private UIHolderObjectBase _holderObjectBase; - - private void Awake() - { - _holderObjectBase = GetComponent(); - _holderObjectBase.OnWindowBeforeShowEvent += BindHotKeys; - _holderObjectBase.OnWindowBeforeClosedEvent += UnBindHotKeys; - } - - private void OnDestroy() - { - if (_holderObjectBase != null) - { - _holderObjectBase.OnWindowBeforeShowEvent -= BindHotKeys; - _holderObjectBase.OnWindowBeforeClosedEvent -= UnBindHotKeys; - } - } - - [SerializeField] private Component[] hotButtons; - - internal void BindHotKeys() - { - if (hotButtons == null) return; - for (int i = 0; i < hotButtons.Length; i++) - { - if (hotButtons[i] is IHotkeyTrigger trigger) - { - trigger.BindHotKey(); - } - } - } - -#if UNITY_EDITOR - [ContextMenu("Bind HotKeys")] - private void CollectUXHotkeys() - { - var found = gameObject - .GetComponentsInChildren(true) - .OfType() - .Where(t => t.HotkeyAction != null) - .Select(t => t as Component) - .ToArray(); - - hotButtons = found; - } - - private void OnValidate() - { - if (_holderObjectBase == null) - { - _holderObjectBase = gameObject.GetComponent(); - CollectUXHotkeys(); - } - } -#endif - - internal void UnBindHotKeys() - { - if (hotButtons == null) return; - for (int i = 0; i < hotButtons.Length; i++) - { - if (hotButtons[i] is IHotkeyTrigger trigger) - { - trigger.UnBindHotKey(); - } - } - } - } -} -#endif diff --git a/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs.meta b/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs.meta deleted file mode 100644 index 98017b4..0000000 --- a/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: fdcda7a93f3c4639a0cb68dd00509bb1 -timeCreated: 1758683821 \ No newline at end of file diff --git a/Runtime/UXComponent/Hotkey/HotkeyComponent.cs b/Runtime/UXComponent/Hotkey/HotkeyComponent.cs index 8a9a18b..0d17b96 100644 --- a/Runtime/UXComponent/Hotkey/HotkeyComponent.cs +++ b/Runtime/UXComponent/Hotkey/HotkeyComponent.cs @@ -1,4 +1,4 @@ -using System; +#if INPUTSYSTEM_SUPPORT using UnityEngine.EventSystems; using UnityEngine.InputSystem; @@ -8,11 +8,8 @@ namespace UnityEngine.UI public sealed class HotkeyComponent : MonoBehaviour, IHotkeyTrigger { [SerializeField] private Component _component; - [SerializeField] private InputActionReference _hotkeyAction; - - [SerializeField] private EHotkeyPressType _hotkeyPressType; - + [SerializeField] private EHotkeyPressType _hotkeyPressType = EHotkeyPressType.Performed; public InputActionReference HotkeyAction { @@ -26,12 +23,66 @@ namespace UnityEngine.UI set => _hotkeyPressType = value; } + private void Reset() + { + AutoAssignTarget(); + } + + private void OnEnable() + { + AutoAssignTarget(); + ((IHotkeyTrigger)this).BindHotKey(); + } + + private void OnDisable() + { + ((IHotkeyTrigger)this).UnBindHotKey(); + } + + private void OnDestroy() + { + ((IHotkeyTrigger)this).UnBindHotKey(); + } + +#if UNITY_EDITOR + private void OnValidate() + { + AutoAssignTarget(); + } +#endif + void IHotkeyTrigger.HotkeyActionTrigger() { - if (_component is ISubmitHandler submitHandler) + if (!isActiveAndEnabled || _component == null) { - submitHandler.OnSubmit(null); + return; + } + + if (_component is ISubmitHandler) + { + ExecuteEvents.Execute( + _component.gameObject, + new BaseEventData(EventSystem.current), + ExecuteEvents.submitHandler + ); + return; + } + + Debug.LogWarning($"{nameof(HotkeyComponent)} target must implement {nameof(ISubmitHandler)}: {_component.name}", this); + } + + private void AutoAssignTarget() + { + if (_component != null) + { + return; + } + + if (TryGetComponent(typeof(ISubmitHandler), out Component submitHandler)) + { + _component = submitHandler; } } } } +#endif diff --git a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs index 19fc848..bcc389d 100644 --- a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs +++ b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs @@ -1,12 +1,10 @@ #if INPUTSYSTEM_SUPPORT using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using AlicizaX.UI.Runtime; using UnityEngine; using UnityEngine.InputSystem; -using Unity.Collections; -using Unity.Collections.LowLevel.Unsafe; -using UnityEngine.UI; namespace UnityEngine.UI { @@ -16,395 +14,638 @@ namespace UnityEngine.UI Performed = 1 } - [StructLayout(LayoutKind.Sequential, Pack = 4)] - internal struct HotkeyRegistration + internal readonly struct HotkeyRegistration { - public IHotkeyTrigger button; - public EHotkeyPressType pressType; + public readonly IHotkeyTrigger Trigger; + public readonly EHotkeyPressType PressType; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public HotkeyRegistration(IHotkeyTrigger btn, EHotkeyPressType pressType) + public HotkeyRegistration(IHotkeyTrigger trigger, EHotkeyPressType pressType) { - this.button = btn; - this.pressType = pressType; + Trigger = trigger; + PressType = pressType; } } - [StructLayout(LayoutKind.Sequential)] - internal struct HandlerInfo + internal sealed class HotkeyScope { - public Action handler; - public InputActionReference action; - - public HandlerInfo(Action handler, InputActionReference action) + public HotkeyScope(UIHolderObjectBase holder) { - this.handler = handler; - this.action = action; + 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 const int INITIAL_CAPACITY = 32; - private const int MAX_REGISTRATIONS_PER_ACTION = 16; + 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 HotkeyRegistration[][] _registrationPool; - private static int[] _registrationCounts; - private static string[] _actionIds; - private static int _actionCount; - private static int _actionCapacity; - - private static HandlerInfo[] _handlers; - private static InputActionReference[] _actionRefs; - - private static IHotkeyTrigger[] _buttons; - private static int[] _buttonToActionIndex; - private static int _buttonCount; - private static int _buttonCapacity; - - private static Action[] _cachedHandlers; - private static int _cachedHandlerCount; - - static UXHotkeyRegisterManager() - { - _actionCapacity = INITIAL_CAPACITY; - _registrationPool = new HotkeyRegistration[_actionCapacity][]; - _registrationCounts = new int[_actionCapacity]; - _actionIds = new string[_actionCapacity]; - _handlers = new HandlerInfo[_actionCapacity]; - _actionRefs = new InputActionReference[_actionCapacity]; - - _buttonCapacity = 64; - _buttons = new IHotkeyTrigger[_buttonCapacity]; - _buttonToActionIndex = new int[_buttonCapacity]; - - _cachedHandlers = new Action[INITIAL_CAPACITY]; - - for (int i = 0; i < _actionCapacity; i++) - { - _registrationPool[i] = new HotkeyRegistration[MAX_REGISTRATIONS_PER_ACTION]; - _buttonToActionIndex[i] = -1; - } - } + private static ulong _serialCounter; #if UNITY_EDITOR [UnityEditor.Callbacks.DidReloadScripts] internal static void ClearHotkeyRegistry() { - for (int i = 0; i < _buttonCount; i++) + IHotkeyTrigger[] triggers = new IHotkeyTrigger[_triggerMap.Count]; + int index = 0; + foreach (var kvp in _triggerMap) { - if (_buttons[i] != null) - { - UnregisterHotkey(_buttons[i]); - } + triggers[index++] = kvp.Key; } - Array.Clear(_registrationCounts, 0, _actionCount); - Array.Clear(_actionIds, 0, _actionCount); - Array.Clear(_handlers, 0, _actionCount); - Array.Clear(_actionRefs, 0, _actionCount); - Array.Clear(_buttons, 0, _buttonCount); - Array.Clear(_buttonToActionIndex, 0, _buttonCount); + for (int i = 0; i < triggers.Length; i++) + { + UnregisterHotkey(triggers[i]); + } - _actionCount = 0; - _buttonCount = 0; - _cachedHandlerCount = 0; + _actions.Clear(); + _triggerMap.Clear(); + _scopes.Clear(); + _leafScopes.Clear(); + _ancestorHolders.Clear(); + _serialCounter = 0; } #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void RegisterHotkey(IHotkeyTrigger button, InputActionReference action, EHotkeyPressType pressType) + internal static void RegisterHotkey(IHotkeyTrigger trigger, UIHolderObjectBase holder, InputActionReference action, EHotkeyPressType pressType) { - if (action == null || action.action == null || button == null) + if (trigger == null || holder == null || action == null || action.action == null) + { return; + } + + UnregisterHotkey(trigger); string actionId = action.action.id.ToString(); - int actionIndex = FindOrCreateActionIndex(actionId); + HotkeyScope scope = GetOrCreateScope(holder); + ActionRegistrationBucket bucket = GetOrCreateBucket(actionId, action); + HotkeyRegistration registration = new HotkeyRegistration(trigger, pressType); - ref int count = ref _registrationCounts[actionIndex]; - if (count >= MAX_REGISTRATIONS_PER_ACTION) + AdjustBucketSubscription(bucket, pressType, true); + AddScopeRegistration(scope, actionId, registration); + + if (scope.LifecycleActive) { - Debug.LogWarning($"Max registrations reached for action: {actionId}"); - return; + scope.ActivationSerial = ++_serialCounter; } - _registrationPool[actionIndex][count] = new HotkeyRegistration(button, pressType); - count++; - - int buttonIndex = FindOrCreateButtonIndex(button); - _buttonToActionIndex[buttonIndex] = actionIndex; - - if (count == 1) - { - Action handler = GetOrCreateHandler(actionIndex); - _handlers[actionIndex] = new HandlerInfo(handler, action); - _actionRefs[actionIndex] = action; - - switch (pressType) - { - case EHotkeyPressType.Started: - action.action.started += handler; - break; - case EHotkeyPressType.Performed: - action.action.performed += handler; - break; - } - - action.action.Enable(); - } + _triggerMap[trigger] = new TriggerRegistration(actionId, holder, pressType); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnregisterHotkey(IHotkeyTrigger button) + internal static void UnregisterHotkey(IHotkeyTrigger trigger) { - if (button == null) - return; - - int buttonIndex = FindButtonIndex(button); - if (buttonIndex < 0) - return; - - int actionIndex = _buttonToActionIndex[buttonIndex]; - if (actionIndex < 0) - return; - - ref int count = ref _registrationCounts[actionIndex]; - HotkeyRegistration[] registrations = _registrationPool[actionIndex]; - - for (int i = count - 1; i >= 0; i--) + if (trigger == null || !_triggerMap.TryGetValue(trigger, out var triggerRegistration)) { - if (ReferenceEquals(registrations[i].button, button)) + 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++) { - EHotkeyPressType pressType = registrations[i].pressType; + triggers ??= new List(registrations.Count); + triggers.Add(registrations[i].Trigger); + } + } - registrations[i] = registrations[count - 1]; - registrations[count - 1] = default; - count--; + if (triggers != null) + { + for (int i = 0; i < triggers.Count; i++) + { + UnregisterHotkey(triggers[i]); + } + } - if (count == 0) - { - ref HandlerInfo handlerInfo = ref _handlers[actionIndex]; - InputActionReference actionRef = _actionRefs[actionIndex]; + DetachScope(scope); + } - if (actionRef != null && actionRef.action != null) - { - switch (pressType) - { - case EHotkeyPressType.Started: - actionRef.action.started -= handlerInfo.handler; - break; - case EHotkeyPressType.Performed: - actionRef.action.performed -= handlerInfo.handler; - break; - } + private static void DetachScope(HotkeyScope scope) + { + if (scope == null || scope.Holder == null) + { + return; + } - actionRef.action.Disable(); - } + scope.Holder.OnWindowBeforeShowEvent -= scope.OnBeforeShow; + scope.Holder.OnWindowBeforeClosedEvent -= scope.OnBeforeClosed; + scope.Holder.OnWindowDestroyEvent -= scope.OnDestroy; + _scopes.Remove(scope.Holder); + } - handlerInfo = default; - _actionRefs[actionIndex] = null; - _actionIds[actionIndex] = null; - } + 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; } } - _buttons[buttonIndex] = null; - _buttonToActionIndex[buttonIndex] = -1; + if (registrations.Count == 0) + { + scope.RegistrationsByAction.Remove(actionId); + } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Action GetOrCreateHandler(int actionIndex) + private static void RemoveActionRegistration(ActionRegistrationBucket bucket, EHotkeyPressType pressType, string actionId) { - if (actionIndex < _cachedHandlers.Length && _cachedHandlers[actionIndex] != null) + AdjustBucketSubscription(bucket, pressType, false); + if (bucket.TotalCount == 0) { - return _cachedHandlers[actionIndex]; + _actions.Remove(actionId); } - - if (actionIndex >= _cachedHandlers.Length) - { - Array.Resize(ref _cachedHandlers, actionIndex + 1); - } - - int capturedIndex = actionIndex; - _cachedHandlers[actionIndex] = ctx => OnHotkeyTriggered(capturedIndex); - return _cachedHandlers[actionIndex]; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void OnHotkeyTriggered(int actionIndex) + private static void AdjustBucketSubscription(ActionRegistrationBucket bucket, EHotkeyPressType pressType, bool add) { - int count = _registrationCounts[actionIndex]; - if (count > 0) + InputAction inputAction = bucket.ActionReference != null ? bucket.ActionReference.action : null; + if (inputAction == null) { - ref HotkeyRegistration registration = ref _registrationPool[actionIndex][count - 1]; - registration.button?.HotkeyActionTrigger(); + 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 int FindOrCreateActionIndex(string actionId) + private static void Dispatch(string actionId, EHotkeyPressType pressType) { - for (int i = 0; i < _actionCount; i++) + HotkeyScope[] leafScopes = GetLeafScopes(); + if (leafScopes.Length == 0) { - if (_actionIds[i] == actionId) - return i; + return; } - if (_actionCount >= _actionCapacity) - { - ExpandActionCapacity(); - } - - int index = _actionCount++; - _actionIds[index] = actionId; - return index; + TryDispatchToScopeChain(leafScopes[0], actionId, pressType); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FindOrCreateButtonIndex(IHotkeyTrigger button) + private static bool TryDispatchToScopeChain(HotkeyScope leafScope, string actionId, EHotkeyPressType pressType) { - for (int i = 0; i < _buttonCount; i++) + UIHolderObjectBase currentHolder = leafScope.Holder; + while (currentHolder != null) { - if (ReferenceEquals(_buttons[i], button)) - return i; - } - - for (int i = 0; i < _buttonCapacity; i++) - { - if (_buttons[i] == null) + if (_scopes.TryGetValue(currentHolder, out var scope) + && TryGetLatestRegistration(scope, actionId, pressType, out var registration)) { - _buttons[i] = button; - if (i >= _buttonCount) - _buttonCount = i + 1; - return i; + 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; + } } } - if (_buttonCount >= _buttonCapacity) - { - ExpandButtonCapacity(); - } - - int index = _buttonCount++; - _buttons[index] = button; - return index; + registration = default; + return false; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FindButtonIndex(IHotkeyTrigger button) + private static HotkeyScope[] GetLeafScopes() { - for (int i = 0; i < _buttonCount; i++) + _leafScopes.Clear(); + _ancestorHolders.Clear(); + + foreach (var scope in _scopes.Values) { - if (ReferenceEquals(_buttons[i], button)) - return i; + if (!IsScopeActive(scope)) + { + continue; + } + + UIHolderObjectBase parentHolder = FindParentHolder(scope.Holder); + while (parentHolder != null) + { + _ancestorHolders.Add(parentHolder); + parentHolder = FindParentHolder(parentHolder); + } } - return -1; + foreach (var scope in _scopes.Values) + { + if (IsScopeActive(scope) && !_ancestorHolders.Contains(scope.Holder)) + { + _leafScopes.Add(scope); + } + } + + _leafScopes.Sort(CompareScopePriority); + return _leafScopes.ToArray(); } - private static void ExpandActionCapacity() + private static bool IsScopeActive(HotkeyScope scope) { - int newCapacity = _actionCapacity * 2; - Array.Resize(ref _registrationPool, newCapacity); - Array.Resize(ref _registrationCounts, newCapacity); - Array.Resize(ref _actionIds, newCapacity); - Array.Resize(ref _handlers, newCapacity); - Array.Resize(ref _actionRefs, newCapacity); - - for (int i = _actionCapacity; i < newCapacity; i++) + if (scope == null || !scope.LifecycleActive) { - _registrationPool[i] = new HotkeyRegistration[MAX_REGISTRATIONS_PER_ACTION]; + return false; } - _actionCapacity = newCapacity; + 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 void ExpandButtonCapacity() + private static bool IsHolderVisible(UIHolderObjectBase holder) { - int newCapacity = _buttonCapacity * 2; - Array.Resize(ref _buttons, newCapacity); - Array.Resize(ref _buttonToActionIndex, newCapacity); - - for (int i = _buttonCapacity; i < newCapacity; i++) + if (holder == null || !holder.gameObject.activeInHierarchy) { - _buttonToActionIndex[i] = -1; + return false; } - _buttonCapacity = newCapacity; + Canvas canvas = holder.GetComponent(); + return canvas != null && canvas.gameObject.layer == UIComponent.UIShowLayer; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RegisterHotkeyBatch(Span buttons, InputActionReference action, EHotkeyPressType pressType) + private static int CompareScopePriority(HotkeyScope left, HotkeyScope right) { - for (int i = 0; i < buttons.Length; i++) + 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) { - RegisterHotkey(buttons[i], action, pressType); + return depthCompare; } + + int hierarchyCompare = right.HierarchyDepth.CompareTo(left.HierarchyDepth); + if (hierarchyCompare != 0) + { + return hierarchyCompare; + } + + return right.ActivationSerial.CompareTo(left.ActivationSerial); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnregisterHotkeyBatch(Span buttons) + private static UIHolderObjectBase FindParentHolder(UIHolderObjectBase holder) { - for (int i = 0; i < buttons.Length; i++) + if (holder == null) { - UnregisterHotkey(buttons[i]); + 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: {_actionCount}/{_actionCapacity}, Buttons: {_buttonCount}/{_buttonCapacity}"; + return $"Actions: {_actions.Count}, Triggers: {_triggerMap.Count}, Scopes: {_scopes.Count}"; } #endif } } -public static class UXHotkeyHotkeyExtension +namespace UnityEngine.UI { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void BindHotKey(this IHotkeyTrigger button) + public static class UXHotkeyHotkeyExtension { - if (button?.HotkeyAction != null) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void BindHotKey(this IHotkeyTrigger trigger) { - UXHotkeyRegisterManager.RegisterHotkey( - button, - button.HotkeyAction, - button.HotkeyPressType - ); + 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 button) - { - if (button?.HotkeyAction != null) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnBindHotKey(this IHotkeyTrigger trigger) { - UXHotkeyRegisterManager.UnregisterHotkey(button); + if (trigger?.HotkeyAction != null) + { + UXHotkeyRegisterManager.UnregisterHotkey(trigger); + } } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void BindHotKeyBatch(this IHotkeyTrigger[] buttons) - { - if (buttons == null) return; - - for (int i = 0; i < buttons.Length; i++) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void BindHotKeyBatch(this IHotkeyTrigger[] triggers) { - buttons[i]?.BindHotKey(); + if (triggers == null) + { + return; + } + + for (int i = 0; i < triggers.Length; i++) + { + triggers[i]?.BindHotKey(); + } } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UnBindHotKeyBatch(this IHotkeyTrigger[] buttons) - { - if (buttons == null) return; - - for (int i = 0; i < buttons.Length; i++) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnBindHotKeyBatch(this IHotkeyTrigger[] triggers) { - buttons[i]?.UnBindHotKey(); + if (triggers == null) + { + return; + } + + for (int i = 0; i < triggers.Length; i++) + { + triggers[i]?.UnBindHotKey(); + } } } }