适配UI框架的热键系统

This commit is contained in:
陈思海 2026-03-20 13:11:41 +08:00
parent e7b0f64c05
commit 4364f4673d
7 changed files with 585 additions and 461 deletions

View File

@ -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<UIHolderObjectBase>();
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();
}
/// <summary>
/// 自定义展开显示 hotButtons 列表
/// </summary>
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--;
}
/// <summary>
/// 查找所有子物体(包含隐藏)并绑定 UXHotkey
/// </summary>
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

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f9779e310e684e9497bdac4f4ac3983e
timeCreated: 1760340548

View File

@ -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<UIHolderObjectBase>(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();

View File

@ -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<UIHolderObjectBase>();
_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<MonoBehaviour>(true)
.OfType<IHotkeyTrigger>()
.Where(t => t.HotkeyAction != null)
.Select(t => t as Component)
.ToArray();
hotButtons = found;
}
private void OnValidate()
{
if (_holderObjectBase == null)
{
_holderObjectBase = gameObject.GetComponent<UIHolderObjectBase>();
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

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: fdcda7a93f3c4639a0cb68dd00509bb1
timeCreated: 1758683821

View File

@ -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

View File

@ -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<InputAction.CallbackContext> handler;
public InputActionReference action;
public HotkeyScope(UIHolderObjectBase holder)
{
Holder = holder;
HierarchyDepth = GetHierarchyDepth(holder.transform);
BlocksLowerScopes = FindParentHolder(holder) == null;
}
public HandlerInfo(Action<InputAction.CallbackContext> handler, InputActionReference action)
public readonly UIHolderObjectBase Holder;
public readonly int HierarchyDepth;
public readonly bool BlocksLowerScopes;
public readonly Dictionary<string, List<HotkeyRegistration>> 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
{
this.handler = handler;
this.action = action;
get
{
if (_canvas == null && Holder != null)
{
_canvas = Holder.GetComponent<Canvas>();
}
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<UIHolderObjectBase>(out var parentHolder))
{
return parentHolder;
}
current = current.parent;
}
return null;
}
}
internal sealed class ActionRegistrationBucket
{
public InputActionReference ActionReference;
public Action<InputAction.CallbackContext> StartedHandler;
public Action<InputAction.CallbackContext> 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<string, ActionRegistrationBucket> _actions = new(StringComparer.Ordinal);
private static readonly Dictionary<IHotkeyTrigger, TriggerRegistration> _triggerMap = new();
private static readonly Dictionary<UIHolderObjectBase, HotkeyScope> _scopes = new();
private static readonly List<HotkeyScope> _leafScopes = new();
private static readonly HashSet<UIHolderObjectBase> _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<InputAction.CallbackContext>[] _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<InputAction.CallbackContext>[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 (trigger == null || holder == null || action == null || action.action == null)
{
if (action == null || action.action == null || button == 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)
{
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))
{
Debug.LogWarning($"Max registrations reached for action: {actionId}");
return;
}
_registrationPool[actionIndex][count] = new HotkeyRegistration(button, pressType);
count++;
int buttonIndex = FindOrCreateButtonIndex(button);
_buttonToActionIndex[buttonIndex] = actionIndex;
if (count == 1)
if (_actions.TryGetValue(triggerRegistration.ActionId, out var bucket))
{
Action<InputAction.CallbackContext> handler = GetOrCreateHandler(actionIndex);
_handlers[actionIndex] = new HandlerInfo(handler, action);
_actionRefs[actionIndex] = action;
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<IHotkeyTrigger> triggers = null;
foreach (var pair in scope.RegistrationsByAction)
{
List<HotkeyRegistration> registrations = pair.Value;
for (int i = 0; i < registrations.Count; i++)
{
triggers ??= new List<IHotkeyTrigger>(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<HotkeyRegistration>();
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:
action.action.started += handler;
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:
action.action.performed += handler;
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;
}
action.action.Enable();
if (bucket.TotalCount > 0)
{
inputAction.Enable();
}
else
{
inputAction.Disable();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnregisterHotkey(IHotkeyTrigger button)
private static void Dispatch(string actionId, EHotkeyPressType pressType)
{
HotkeyScope[] leafScopes = GetLeafScopes();
if (leafScopes.Length == 0)
{
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 (ReferenceEquals(registrations[i].button, button))
{
EHotkeyPressType pressType = registrations[i].pressType;
registrations[i] = registrations[count - 1];
registrations[count - 1] = default;
count--;
if (count == 0)
{
ref HandlerInfo handlerInfo = ref _handlers[actionIndex];
InputActionReference actionRef = _actionRefs[actionIndex];
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;
}
actionRef.action.Disable();
TryDispatchToScopeChain(leafScopes[0], actionId, pressType);
}
handlerInfo = default;
_actionRefs[actionIndex] = null;
_actionIds[actionIndex] = null;
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;
}
break;
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;
}
}
}
_buttons[buttonIndex] = null;
_buttonToActionIndex[buttonIndex] = -1;
registration = default;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Action<InputAction.CallbackContext> GetOrCreateHandler(int actionIndex)
private static HotkeyScope[] GetLeafScopes()
{
if (actionIndex < _cachedHandlers.Length && _cachedHandlers[actionIndex] != null)
_leafScopes.Clear();
_ancestorHolders.Clear();
foreach (var scope in _scopes.Values)
{
return _cachedHandlers[actionIndex];
if (!IsScopeActive(scope))
{
continue;
}
if (actionIndex >= _cachedHandlers.Length)
UIHolderObjectBase parentHolder = FindParentHolder(scope.Holder);
while (parentHolder != null)
{
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)
{
int count = _registrationCounts[actionIndex];
if (count > 0)
{
ref HotkeyRegistration registration = ref _registrationPool[actionIndex][count - 1];
registration.button?.HotkeyActionTrigger();
_ancestorHolders.Add(parentHolder);
parentHolder = FindParentHolder(parentHolder);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FindOrCreateActionIndex(string actionId)
foreach (var scope in _scopes.Values)
{
for (int i = 0; i < _actionCount; i++)
if (IsScopeActive(scope) && !_ancestorHolders.Contains(scope.Holder))
{
if (_actionIds[i] == actionId)
return i;
}
if (_actionCount >= _actionCapacity)
{
ExpandActionCapacity();
}
int index = _actionCount++;
_actionIds[index] = actionId;
return index;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FindOrCreateButtonIndex(IHotkeyTrigger button)
{
for (int i = 0; i < _buttonCount; i++)
{
if (ReferenceEquals(_buttons[i], button))
return i;
}
for (int i = 0; i < _buttonCapacity; i++)
{
if (_buttons[i] == null)
{
_buttons[i] = button;
if (i >= _buttonCount)
_buttonCount = i + 1;
return i;
_leafScopes.Add(scope);
}
}
if (_buttonCount >= _buttonCapacity)
{
ExpandButtonCapacity();
_leafScopes.Sort(CompareScopePriority);
return _leafScopes.ToArray();
}
int index = _buttonCount++;
_buttons[index] = button;
return index;
private static bool IsScopeActive(HotkeyScope scope)
{
if (scope == null || !scope.LifecycleActive)
{
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FindButtonIndex(IHotkeyTrigger button)
UIHolderObjectBase holder = scope.Holder;
if (holder == null || !holder.IsValid())
{
for (int i = 0; i < _buttonCount; i++)
{
if (ReferenceEquals(_buttons[i], button))
return i;
return false;
}
return -1;
if (!holder.gameObject.activeInHierarchy)
{
return false;
}
private static void ExpandActionCapacity()
{
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++)
{
_registrationPool[i] = new HotkeyRegistration[MAX_REGISTRATIONS_PER_ACTION];
Canvas canvas = scope.Canvas;
return canvas != null && canvas.gameObject.layer == UIComponent.UIShowLayer;
}
_actionCapacity = newCapacity;
private static bool IsHolderVisible(UIHolderObjectBase holder)
{
if (holder == null || !holder.gameObject.activeInHierarchy)
{
return false;
}
private static void ExpandButtonCapacity()
{
int newCapacity = _buttonCapacity * 2;
Array.Resize(ref _buttons, newCapacity);
Array.Resize(ref _buttonToActionIndex, newCapacity);
for (int i = _buttonCapacity; i < newCapacity; i++)
{
_buttonToActionIndex[i] = -1;
Canvas canvas = holder.GetComponent<Canvas>();
return canvas != null && canvas.gameObject.layer == UIComponent.UIShowLayer;
}
_buttonCapacity = newCapacity;
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;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RegisterHotkeyBatch(Span<IHotkeyTrigger> buttons, InputActionReference action, EHotkeyPressType pressType)
int hierarchyCompare = right.HierarchyDepth.CompareTo(left.HierarchyDepth);
if (hierarchyCompare != 0)
{
for (int i = 0; i < buttons.Length; i++)
{
RegisterHotkey(buttons[i], action, pressType);
}
return hierarchyCompare;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnregisterHotkeyBatch(Span<IHotkeyTrigger> buttons)
{
for (int i = 0; i < buttons.Length; i++)
{
UnregisterHotkey(buttons[i]);
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<UIHolderObjectBase>(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
}
}
namespace UnityEngine.UI
{
public static class UXHotkeyHotkeyExtension
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void BindHotKey(this IHotkeyTrigger button)
public static void BindHotKey(this IHotkeyTrigger trigger)
{
if (button?.HotkeyAction != null)
if (trigger?.HotkeyAction == null)
{
UXHotkeyRegisterManager.RegisterHotkey(
button,
button.HotkeyAction,
button.HotkeyPressType
);
return;
}
if (trigger is not Component component)
{
return;
}
UIHolderObjectBase holder = component.GetComponentInParent<UIHolderObjectBase>(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 UnBindHotKey(this IHotkeyTrigger button)
public static void BindHotKeyBatch(this IHotkeyTrigger[] triggers)
{
if (button?.HotkeyAction != null)
if (triggers == null)
{
UXHotkeyRegisterManager.UnregisterHotkey(button);
return;
}
for (int i = 0; i < triggers.Length; i++)
{
triggers[i]?.BindHotKey();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void BindHotKeyBatch(this IHotkeyTrigger[] buttons)
public static void UnBindHotKeyBatch(this IHotkeyTrigger[] triggers)
{
if (buttons == null) return;
for (int i = 0; i < buttons.Length; i++)
if (triggers == null)
{
buttons[i]?.BindHotKey();
}
return;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnBindHotKeyBatch(this IHotkeyTrigger[] buttons)
for (int i = 0; i < triggers.Length; i++)
{
if (buttons == null) return;
for (int i = 0; i < buttons.Length; i++)
{
buttons[i]?.UnBindHotKey();
triggers[i]?.UnBindHotKey();
}
}
}
}