Compare commits

...

3 Commits

Author SHA1 Message Date
4364f4673d 适配UI框架的热键系统 2026-03-20 13:11:41 +08:00
e7b0f64c05 Update HotkeyComponent.cs 2026-03-19 20:16:58 +08:00
bc554a5062 分离Hotkey脱离扩展组件 2026-03-19 19:50:32 +08:00
14 changed files with 667 additions and 568 deletions

View File

@ -20,12 +20,6 @@ namespace UnityEditor.UI
{ {
SerializedProperty m_OnClickProperty; SerializedProperty m_OnClickProperty;
#if INPUTSYSTEM_SUPPORT
private SerializedProperty _hotKeyRefrence;
private SerializedProperty _hotkeyPressType;
#endif
private SerializedProperty hoverAudioClip; private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip; private SerializedProperty clickAudioClip;
@ -35,12 +29,6 @@ namespace UnityEditor.UI
m_OnClickProperty = serializedObject.FindProperty("m_OnClick"); m_OnClickProperty = serializedObject.FindProperty("m_OnClick");
#if INPUTSYSTEM_SUPPORT
_hotKeyRefrence = serializedObject.FindProperty("_hotkeyAction");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
#endif
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip"); hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
clickAudioClip = serializedObject.FindProperty("clickAudioClip"); clickAudioClip = serializedObject.FindProperty("clickAudioClip");
@ -64,15 +52,6 @@ namespace UnityEditor.UI
serializedObject.Update(); serializedObject.Update();
EditorGUILayout.PropertyField(m_OnClickProperty); EditorGUILayout.PropertyField(m_OnClickProperty);
#if INPUTSYSTEM_SUPPORT
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField("Hotkey Setting", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_hotKeyRefrence, new GUIContent("InputAction"));
EditorGUILayout.PropertyField(_hotkeyPressType, new GUIContent("PressType"));
}
#endif
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
} }

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

@ -0,0 +1,65 @@
using AlicizaX.UI.Runtime;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UnityEditor.UI
{
[CustomEditor(typeof(HotkeyComponent), true)]
public class HotkeyComponentEditor : UnityEditor.Editor
{
private SerializedProperty _hotkeyAction;
private SerializedProperty _hotkeyPressType;
private SerializedProperty _component;
private void OnEnable()
{
_component = serializedObject.FindProperty("_component");
_hotkeyAction = serializedObject.FindProperty("_hotkeyAction");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
}
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("No submit target was found on this object.", MessageType.Error);
if (hotkeyComponent.TryGetComponent(typeof(ISubmitHandler), out Component submitHandler))
{
_component.objectReferenceValue = submitHandler;
}
}
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField("Hotkey Setting", EditorStyles.boldLabel);
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.PropertyField(_component, new GUIContent("Component"));
EditorGUI.EndDisabledGroup();
EditorGUILayout.PropertyField(_hotkeyAction, new GUIContent("Input Action"));
EditorGUILayout.PropertyField(_hotkeyPressType, new GUIContent("Press Type"));
}
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 939657bb1b5f4ad5b0836fa8fae2bef3
timeCreated: 1773919464

View File

@ -17,11 +17,6 @@ namespace UnityEditor.UI
SerializedProperty m_GroupProperty; SerializedProperty m_GroupProperty;
SerializedProperty m_IsOnProperty; SerializedProperty m_IsOnProperty;
#if INPUTSYSTEM_SUPPORT
private SerializedProperty _hotKeyRefrence;
private SerializedProperty _hotkeyPressType;
#endif
private SerializedProperty hoverAudioClip; private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip; private SerializedProperty clickAudioClip;
@ -35,11 +30,6 @@ namespace UnityEditor.UI
m_IsOnProperty = serializedObject.FindProperty("m_IsOn"); m_IsOnProperty = serializedObject.FindProperty("m_IsOn");
m_OnValueChangedProperty = serializedObject.FindProperty("onValueChanged"); m_OnValueChangedProperty = serializedObject.FindProperty("onValueChanged");
#if INPUTSYSTEM_SUPPORT
_hotKeyRefrence = serializedObject.FindProperty("_hotkeyAction");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
#endif
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip"); hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
clickAudioClip = serializedObject.FindProperty("clickAudioClip"); clickAudioClip = serializedObject.FindProperty("clickAudioClip");
@ -62,15 +52,6 @@ namespace UnityEditor.UI
serializedObject.Update(); serializedObject.Update();
EditorGUILayout.PropertyField(m_OnValueChangedProperty); EditorGUILayout.PropertyField(m_OnValueChangedProperty);
#if INPUTSYSTEM_SUPPORT
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField("Hotkey Setting", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_hotKeyRefrence, new GUIContent("InputAction"));
EditorGUILayout.PropertyField(_hotkeyPressType, new GUIContent("PressType"));
}
#endif
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
} }

View File

@ -5,48 +5,11 @@ using AlicizaX.UI;
using AlicizaX.UI.Extension; using AlicizaX.UI.Extension;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
#if INPUTSYSTEM_SUPPORT
using UnityEngine.InputSystem;
#endif
namespace UnityEngine.UI namespace UnityEngine.UI
{ {
[AddComponentMenu("UI/UXButton", 30)] [AddComponentMenu("UI/UXButton", 30)]
public class UXButton : UXSelectable, IPointerClickHandler, ISubmitHandler, IButton public class UXButton : UXSelectable, IPointerClickHandler, ISubmitHandler, IButton
#if INPUTSYSTEM_SUPPORT
, IHotkeyTrigger
#endif
{ {
#if INPUTSYSTEM_SUPPORT
InputActionReference IHotkeyTrigger.HotkeyAction
{
get => _hotkeyAction;
set => _hotkeyAction = value;
}
EHotkeyPressType IHotkeyTrigger.HotkeyPressType
{
get => _hotkeyPressType;
set => _hotkeyPressType = value;
}
void IHotkeyTrigger.HotkeyActionTrigger()
{
if (interactable)
{
OnSubmit(null);
}
}
[SerializeField] internal InputActionReference _hotkeyAction;
[SerializeField] internal EHotkeyPressType _hotkeyPressType;
public InputActionReference HotKeyRefrence
{
get { return _hotkeyAction; }
}
#endif
[SerializeField] private AudioClip hoverAudioClip; [SerializeField] private AudioClip hoverAudioClip;
[SerializeField] private AudioClip clickAudioClip; [SerializeField] private AudioClip clickAudioClip;

View File

@ -1,22 +1,12 @@
using System; using System;
using UnityEngine;
using UnityEngine.Events; using UnityEngine.Events;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
#if INPUTSYSTEM_SUPPORT
using UnityEngine.InputSystem;
#endif
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEngine.UI namespace UnityEngine.UI
{ {
[AddComponentMenu("UI/UXToggle", 30)] [AddComponentMenu("UI/UXToggle", 30)]
[RequireComponent(typeof(RectTransform))] [RequireComponent(typeof(RectTransform))]
public class UXToggle : UXSelectable, IPointerClickHandler, ISubmitHandler, ICanvasElement public class UXToggle : UXSelectable, IPointerClickHandler, ISubmitHandler, ICanvasElement
#if INPUTSYSTEM_SUPPORT
, IHotkeyTrigger
#endif
{ {
[Serializable] [Serializable]
public class ToggleEvent : UnityEvent<bool> public class ToggleEvent : UnityEvent<bool>
@ -249,36 +239,6 @@ namespace UnityEngine.UI
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip); UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
} }
#if INPUTSYSTEM_SUPPORT
InputActionReference IHotkeyTrigger.HotkeyAction
{
get => _hotkeyAction;
set => _hotkeyAction = value;
}
EHotkeyPressType IHotkeyTrigger.HotkeyPressType
{
get => _hotkeyPressType;
set => _hotkeyPressType = value;
}
void IHotkeyTrigger.HotkeyActionTrigger()
{
if (interactable)
{
OnSubmit(null);
}
}
[SerializeField] internal InputActionReference _hotkeyAction;
[SerializeField] internal EHotkeyPressType _hotkeyPressType;
public InputActionReference HotKeyRefrence
{
get { return _hotkeyAction; }
}
#endif
[SerializeField] private AudioClip hoverAudioClip; [SerializeField] private AudioClip hoverAudioClip;
[SerializeField] private AudioClip clickAudioClip; [SerializeField] private AudioClip clickAudioClip;

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

@ -0,0 +1,88 @@
#if INPUTSYSTEM_SUPPORT
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
namespace UnityEngine.UI
{
[DisallowMultipleComponent]
public sealed class HotkeyComponent : MonoBehaviour, IHotkeyTrigger
{
[SerializeField] private Component _component;
[SerializeField] private InputActionReference _hotkeyAction;
[SerializeField] private EHotkeyPressType _hotkeyPressType = EHotkeyPressType.Performed;
public InputActionReference HotkeyAction
{
get => _hotkeyAction;
set => _hotkeyAction = value;
}
EHotkeyPressType IHotkeyTrigger.HotkeyPressType
{
get => _hotkeyPressType;
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 (!isActiveAndEnabled || _component == 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

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ad0e473486c04dc19b468f2b20675091
timeCreated: 1773912951

View File

@ -5,7 +5,7 @@ namespace UnityEngine.UI
{ {
public interface IHotkeyTrigger public interface IHotkeyTrigger
{ {
internal InputActionReference HotkeyAction { get; set; } public InputActionReference HotkeyAction { get; }
internal EHotkeyPressType HotkeyPressType { get; set; } internal EHotkeyPressType HotkeyPressType { get; set; }
internal void HotkeyActionTrigger(); internal void HotkeyActionTrigger();
} }

View File

@ -1,12 +1,10 @@
#if INPUTSYSTEM_SUPPORT #if INPUTSYSTEM_SUPPORT
using System; using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using AlicizaX.UI.Runtime;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.UI;
namespace UnityEngine.UI namespace UnityEngine.UI
{ {
@ -16,395 +14,638 @@ namespace UnityEngine.UI
Performed = 1 Performed = 1
} }
[StructLayout(LayoutKind.Sequential, Pack = 4)] internal readonly struct HotkeyRegistration
internal struct HotkeyRegistration
{ {
public IHotkeyTrigger button; public readonly IHotkeyTrigger Trigger;
public EHotkeyPressType pressType; public readonly EHotkeyPressType PressType;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public HotkeyRegistration(IHotkeyTrigger btn, EHotkeyPressType pressType) public HotkeyRegistration(IHotkeyTrigger trigger, EHotkeyPressType pressType)
{ {
this.button = btn; Trigger = trigger;
this.pressType = pressType; PressType = pressType;
} }
} }
[StructLayout(LayoutKind.Sequential)] internal sealed class HotkeyScope
internal struct HandlerInfo
{ {
public Action<InputAction.CallbackContext> handler; public HotkeyScope(UIHolderObjectBase holder)
public InputActionReference action;
public HandlerInfo(Action<InputAction.CallbackContext> handler, InputActionReference action)
{ {
this.handler = handler; Holder = holder;
this.action = action; HierarchyDepth = GetHierarchyDepth(holder.transform);
BlocksLowerScopes = FindParentHolder(holder) == null;
}
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
{
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 internal static class UXHotkeyRegisterManager
{ {
private const int INITIAL_CAPACITY = 32; private static readonly Dictionary<string, ActionRegistrationBucket> _actions = new(StringComparer.Ordinal);
private const int MAX_REGISTRATIONS_PER_ACTION = 16; 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 ulong _serialCounter;
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;
}
}
#if UNITY_EDITOR #if UNITY_EDITOR
[UnityEditor.Callbacks.DidReloadScripts] [UnityEditor.Callbacks.DidReloadScripts]
internal static void ClearHotkeyRegistry() 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) triggers[index++] = kvp.Key;
{
UnregisterHotkey(_buttons[i]);
}
} }
Array.Clear(_registrationCounts, 0, _actionCount); for (int i = 0; i < triggers.Length; i++)
Array.Clear(_actionIds, 0, _actionCount); {
Array.Clear(_handlers, 0, _actionCount); UnregisterHotkey(triggers[i]);
Array.Clear(_actionRefs, 0, _actionCount); }
Array.Clear(_buttons, 0, _buttonCount);
Array.Clear(_buttonToActionIndex, 0, _buttonCount);
_actionCount = 0; _actions.Clear();
_buttonCount = 0; _triggerMap.Clear();
_cachedHandlerCount = 0; _scopes.Clear();
_leafScopes.Clear();
_ancestorHolders.Clear();
_serialCounter = 0;
} }
#endif #endif
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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; return;
}
UnregisterHotkey(trigger);
string actionId = action.action.id.ToString(); 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]; AdjustBucketSubscription(bucket, pressType, true);
if (count >= MAX_REGISTRATIONS_PER_ACTION) AddScopeRegistration(scope, actionId, registration);
if (scope.LifecycleActive)
{ {
Debug.LogWarning($"Max registrations reached for action: {actionId}"); scope.ActivationSerial = ++_serialCounter;
return;
} }
_registrationPool[actionIndex][count] = new HotkeyRegistration(button, pressType); _triggerMap[trigger] = new TriggerRegistration(actionId, holder, pressType);
count++;
int buttonIndex = FindOrCreateButtonIndex(button);
_buttonToActionIndex[buttonIndex] = actionIndex;
if (count == 1)
{
Action<InputAction.CallbackContext> 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();
}
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnregisterHotkey(IHotkeyTrigger button) internal static void UnregisterHotkey(IHotkeyTrigger trigger)
{ {
if (button == null) if (trigger == null || !_triggerMap.TryGetValue(trigger, out var triggerRegistration))
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)) 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<IHotkeyTrigger> triggers = null;
foreach (var pair in scope.RegistrationsByAction)
{
List<HotkeyRegistration> registrations = pair.Value;
for (int i = 0; i < registrations.Count; i++)
{ {
EHotkeyPressType pressType = registrations[i].pressType; triggers ??= new List<IHotkeyTrigger>(registrations.Count);
triggers.Add(registrations[i].Trigger);
}
}
registrations[i] = registrations[count - 1]; if (triggers != null)
registrations[count - 1] = default; {
count--; for (int i = 0; i < triggers.Count; i++)
{
UnregisterHotkey(triggers[i]);
}
}
if (count == 0) DetachScope(scope);
{ }
ref HandlerInfo handlerInfo = ref _handlers[actionIndex];
InputActionReference actionRef = _actionRefs[actionIndex];
if (actionRef != null && actionRef.action != null) private static void DetachScope(HotkeyScope scope)
{ {
switch (pressType) if (scope == null || scope.Holder == null)
{ {
case EHotkeyPressType.Started: return;
actionRef.action.started -= handlerInfo.handler; }
break;
case EHotkeyPressType.Performed:
actionRef.action.performed -= handlerInfo.handler;
break;
}
actionRef.action.Disable(); scope.Holder.OnWindowBeforeShowEvent -= scope.OnBeforeShow;
} scope.Holder.OnWindowBeforeClosedEvent -= scope.OnBeforeClosed;
scope.Holder.OnWindowDestroyEvent -= scope.OnDestroy;
_scopes.Remove(scope.Holder);
}
handlerInfo = default; private static void ReleaseScopeIfEmpty(HotkeyScope scope)
_actionRefs[actionIndex] = null; {
_actionIds[actionIndex] = null; 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; break;
} }
} }
_buttons[buttonIndex] = null; if (registrations.Count == 0)
_buttonToActionIndex[buttonIndex] = -1; {
scope.RegistrationsByAction.Remove(actionId);
}
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] private static void RemoveActionRegistration(ActionRegistrationBucket bucket, EHotkeyPressType pressType, string actionId)
private static Action<InputAction.CallbackContext> GetOrCreateHandler(int actionIndex)
{ {
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 AdjustBucketSubscription(ActionRegistrationBucket bucket, EHotkeyPressType pressType, bool add)
private static void OnHotkeyTriggered(int actionIndex)
{ {
int count = _registrationCounts[actionIndex]; InputAction inputAction = bucket.ActionReference != null ? bucket.ActionReference.action : null;
if (count > 0) if (inputAction == null)
{ {
ref HotkeyRegistration registration = ref _registrationPool[actionIndex][count - 1]; return;
registration.button?.HotkeyActionTrigger(); }
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)] [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;
return i;
} }
if (_actionCount >= _actionCapacity) TryDispatchToScopeChain(leafScopes[0], actionId, pressType);
{
ExpandActionCapacity();
}
int index = _actionCount++;
_actionIds[index] = actionId;
return index;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryDispatchToScopeChain(HotkeyScope leafScope, string actionId, EHotkeyPressType pressType)
private static int FindOrCreateButtonIndex(IHotkeyTrigger button)
{ {
for (int i = 0; i < _buttonCount; i++) UIHolderObjectBase currentHolder = leafScope.Holder;
while (currentHolder != null)
{ {
if (ReferenceEquals(_buttons[i], button)) if (_scopes.TryGetValue(currentHolder, out var scope)
return i; && TryGetLatestRegistration(scope, actionId, pressType, out var registration))
}
for (int i = 0; i < _buttonCapacity; i++)
{
if (_buttons[i] == null)
{ {
_buttons[i] = button; registration.Trigger?.HotkeyActionTrigger();
if (i >= _buttonCount) return true;
_buttonCount = i + 1; }
return i;
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) registration = default;
{ return false;
ExpandButtonCapacity();
}
int index = _buttonCount++;
_buttons[index] = button;
return index;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] private static HotkeyScope[] GetLeafScopes()
private static int FindButtonIndex(IHotkeyTrigger button)
{ {
for (int i = 0; i < _buttonCount; i++) _leafScopes.Clear();
_ancestorHolders.Clear();
foreach (var scope in _scopes.Values)
{ {
if (ReferenceEquals(_buttons[i], button)) if (!IsScopeActive(scope))
return i; {
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; if (scope == null || !scope.LifecycleActive)
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]; 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; if (holder == null || !holder.gameObject.activeInHierarchy)
Array.Resize(ref _buttons, newCapacity);
Array.Resize(ref _buttonToActionIndex, newCapacity);
for (int i = _buttonCapacity; i < newCapacity; i++)
{ {
_buttonToActionIndex[i] = -1; return false;
} }
_buttonCapacity = newCapacity; Canvas canvas = holder.GetComponent<Canvas>();
return canvas != null && canvas.gameObject.layer == UIComponent.UIShowLayer;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] private static int CompareScopePriority(HotkeyScope left, HotkeyScope right)
public static void RegisterHotkeyBatch(Span<IHotkeyTrigger> buttons, InputActionReference action, EHotkeyPressType pressType)
{ {
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)] private static UIHolderObjectBase FindParentHolder(UIHolderObjectBase holder)
public static void UnregisterHotkeyBatch(Span<IHotkeyTrigger> buttons)
{ {
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<UIHolderObjectBase>(out var parentHolder))
{
return parentHolder;
}
current = current.parent;
}
return null;
} }
#if UNITY_EDITOR #if UNITY_EDITOR
public static string GetDebugInfo() public static string GetDebugInfo()
{ {
return $"Actions: {_actionCount}/{_actionCapacity}, Buttons: {_buttonCount}/{_buttonCapacity}"; return $"Actions: {_actions.Count}, Triggers: {_triggerMap.Count}, Scopes: {_scopes.Count}";
} }
#endif #endif
} }
} }
public static class UXHotkeyHotkeyExtension namespace UnityEngine.UI
{ {
[MethodImpl(MethodImplOptions.AggressiveInlining)] public static class UXHotkeyHotkeyExtension
public static void BindHotKey(this IHotkeyTrigger button)
{ {
if (button?.HotkeyAction != null) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void BindHotKey(this IHotkeyTrigger trigger)
{ {
UXHotkeyRegisterManager.RegisterHotkey( if (trigger?.HotkeyAction == null)
button, {
button.HotkeyAction, return;
button.HotkeyPressType }
);
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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnBindHotKey(this IHotkeyTrigger button) public static void UnBindHotKey(this IHotkeyTrigger trigger)
{
if (button?.HotkeyAction != null)
{ {
UXHotkeyRegisterManager.UnregisterHotkey(button); if (trigger?.HotkeyAction != null)
{
UXHotkeyRegisterManager.UnregisterHotkey(trigger);
}
} }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void BindHotKeyBatch(this IHotkeyTrigger[] buttons) public static void BindHotKeyBatch(this IHotkeyTrigger[] triggers)
{
if (buttons == null) return;
for (int i = 0; i < buttons.Length; i++)
{ {
buttons[i]?.BindHotKey(); if (triggers == null)
{
return;
}
for (int i = 0; i < triggers.Length; i++)
{
triggers[i]?.BindHotKey();
}
} }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnBindHotKeyBatch(this IHotkeyTrigger[] buttons) public static void UnBindHotKeyBatch(this IHotkeyTrigger[] triggers)
{
if (buttons == null) return;
for (int i = 0; i < buttons.Length; i++)
{ {
buttons[i]?.UnBindHotKey(); if (triggers == null)
{
return;
}
for (int i = 0; i < triggers.Length; i++)
{
triggers[i]?.UnBindHotKey();
}
} }
} }
} }