com.alicizax.unity.ui.exten.../Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs

655 lines
20 KiB
C#

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