This commit is contained in:
陈思海 2025-10-13 20:20:01 +08:00
parent 8bbfa59524
commit d8fd91d32c
15 changed files with 592 additions and 565 deletions

3
Editor/UX/Hotkey.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: be711d9c48d440c4afccffac091ccc7d
timeCreated: 1760340542

View File

@ -0,0 +1,111 @@
#if INPUTSYSTEM_SUPPORT
using System.Collections.Generic;
using System.Reflection;
using AlicizaX.Editor;
using AlicizaX.UI.Extension;
using AlicizaX.UI.Extension.UXComponent.Hotkey;
using AlicizaX.UI.Runtime;
using UnityEditor;
using UnityEngine;
namespace UnityEngine.UI.Hotkey
{
[CustomEditor(typeof(HotkeyBindComponent))]
public class HotkeyBindComponentInspector : GameFrameworkInspector
{
private SerializedProperty hotButtonsProp;
private HotkeyBindComponent _target;
private void OnEnable()
{
_target = (HotkeyBindComponent)target;
hotButtonsProp = serializedObject.FindProperty("hotButtons");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// 🔸 检查是否缺少 UIHolderObjectBase
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.arraySize == 0)
{
EditorGUILayout.HelpBox("当前没有绑定任何 UXHotkey。", MessageType.Info);
return;
}
EditorGUI.indentLevel++;
for (int i = 0; i < listProp.arraySize; i++)
{
var element = listProp.GetArrayElementAtIndex(i);
var uxHotkey = element.objectReferenceValue as UXHotkey;
string name = uxHotkey != null ? uxHotkey.name : "Null";
EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXHotkey), true);
}
EditorGUI.indentLevel--;
}
/// <summary>
/// 查找所有子物体(包含隐藏)并绑定 UXHotkey
/// </summary>
private void FindAllUXHotkeys()
{
Undo.RecordObject(_target, "Scan UXHotkey");
var uxHotkeys = _target.GetComponentsInChildren<UXHotkey>(true);
List<UXHotkey> valiedHotkeys = new List<UXHotkey>();
foreach (var item in uxHotkeys)
{
var field = item.GetType()
.GetField("_hotKeyRefrence", BindingFlags.NonPublic | BindingFlags.Instance);
var hotRefrenceObject = field.GetValue(item);
if (hotRefrenceObject != null) valiedHotkeys.Add(item);
}
_target.GetType()
.GetField("hotButtons", BindingFlags.NonPublic | BindingFlags.Instance)
?.SetValue(_target, valiedHotkeys.ToArray());
EditorUtility.SetDirty(_target);
serializedObject.Update();
Debug.Log($"[HotkeyBindComponent] 已找到 {uxHotkeys.Length} 个 UXHotkey 组件并绑定。");
}
}
}
#endif

View File

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

View File

@ -0,0 +1,45 @@
#if INPUTSYSTEM_SUPPORT
using AlicizaX.Editor;
using AlicizaX.UI.Extension;
using UnityEditor;
namespace UnityEngine.UI.Hotkey
{
[CustomEditor(typeof(UXHotkey))]
public class HotkeyInspector : GameFrameworkInspector
{
private UXHotkey _target;
private SerializedProperty _button;
private SerializedProperty _hotKeyRefrence;
private SerializedProperty _hotkeyPressType;
private void OnEnable()
{
_target = (UXHotkey)target;
_button = serializedObject.FindProperty("_button");
_hotKeyRefrence = serializedObject.FindProperty("_hotKeyRefrence");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
var holder = _target.GetComponent<UXButton>();
if (holder == null)
{
EditorGUILayout.HelpBox(
"⚠ 当前对象缺少 UXButton 组件",
MessageType.Error
);
}
EditorGUILayout.PropertyField(_button);
EditorGUILayout.PropertyField(_hotKeyRefrence);
EditorGUILayout.PropertyField(_hotkeyPressType);
serializedObject.ApplyModifiedProperties();
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 87a10dc3a1544ec586ff27809213a127
timeCreated: 1760341800

View File

@ -42,15 +42,11 @@ internal class UXButtonEditor : Editor
private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip;
#if INPUTSYSTEM_SUPPORT
private SerializedProperty _hotKeyRefrence;
private SerializedProperty _hotkeyPressType;
#endif
private UXButton mTarget;
private void OnEnable()
{
mTarget = (UXButton)target;
customSkin = AssetDatabase.LoadAssetAtPath<GUISkin>("Packages/com.alicizax.unity.ui.extension/Editor/Res/GUISkin/UIExtensionGUISkin.guiskin");
m_Interactable = serializedObject.FindProperty("m_Interactable");
m_UXGroup = serializedObject.FindProperty("m_UXGroup");
@ -67,10 +63,6 @@ internal class UXButtonEditor : Editor
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
clickAudioClip = serializedObject.FindProperty("clickAudioClip");
#if INPUTSYSTEM_SUPPORT
_hotKeyRefrence = serializedObject.FindProperty("_hotKeyRefrence");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
#endif
CreateChildTransitionList();
}
@ -199,6 +191,11 @@ internal class UXButtonEditor : Editor
EditorGUI.EndDisabledGroup();
var interactable = GUILayoutHelper.DrawToggle(m_Interactable.boolValue, customSkin, "Interactable");
if (interactable != m_Interactable.boolValue)
{
mTarget.Interactable = interactable;
m_SelectionState.enumValueIndex = interactable ? 0 : 4;
}
m_Interactable.boolValue = interactable;
GUILayout.Space(1);
@ -225,10 +222,6 @@ internal class UXButtonEditor : Editor
}
EditorGUILayout.Separator();
#if INPUTSYSTEM_SUPPORT
EditorGUILayout.PropertyField(_hotkeyPressType);
EditorGUILayout.PropertyField(_hotKeyRefrence);
#endif
}
private void DrawAudioTab()
@ -242,9 +235,9 @@ internal class UXButtonEditor : Editor
});
GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () =>
{
if (hoverAudioClip.objectReferenceValue != null)
if (clickAudioClip.objectReferenceValue != null)
{
PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue);
PlayAudio((AudioClip)clickAudioClip.objectReferenceValue);
}
});
}

View File

@ -0,0 +1,50 @@
using AlicizaX.Editor;
using UnityEditor;
namespace UnityEngine.UI
{
[CustomEditor(typeof(UXGroup))]
public class UXGroupInspector : GameFrameworkInspector
{
private SerializedProperty m_Buttons;
private UXGroup _target;
private void OnEnable()
{
_target = (UXGroup)target;
m_Buttons = serializedObject.FindProperty("m_Buttons");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
GUI.enabled = false;
DrawHotButtonListAlwaysExpanded(m_Buttons);
GUI.enabled = true;
EditorGUILayout.Space();
serializedObject.ApplyModifiedProperties();
}
private void DrawHotButtonListAlwaysExpanded(SerializedProperty listProp)
{
EditorGUILayout.LabelField("Hot Buttons", EditorStyles.boldLabel);
if (listProp.arraySize == 0)
{
EditorGUILayout.HelpBox("当前没有绑定任何 UXButton", MessageType.Info);
return;
}
for (int i = 0; i < listProp.arraySize; i++)
{
var element = listProp.GetArrayElementAtIndex(i);
var uxButton = element.objectReferenceValue as UXButton;
string name = uxButton != null ? uxButton.name : "Null";
EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXButton), true);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 292ca921cd4242d4be9f76bef9bfc08f
timeCreated: 1760343702

View File

@ -1,14 +1,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
using AlicizaX;
using AlicizaX.UI.Extension;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
using UnityEngine.UI;
[Serializable]
public enum ButtonModeType
{
@ -16,12 +15,12 @@ public enum ButtonModeType
Toggle
}
[System.Serializable]
[Serializable]
public class TransitionData
{
public Graphic targetGraphic;
public Selectable.Transition transition = Selectable.Transition.ColorTint;
public ColorBlock colors;
public ColorBlock colors = ColorBlock.defaultColorBlock;
public SpriteState spriteState;
public AnimationTriggers animationTriggers = new();
}
@ -51,69 +50,55 @@ public class UXButton : UIBehaviour, IButton,
[SerializeField] private UXGroup m_UXGroup;
[SerializeField] private AudioClip hoverAudioClip;
[SerializeField] private AudioClip clickAudioClip;
#if INPUTSYSTEM_SUPPORT
[SerializeField] internal UnityEngine.InputSystem.InputActionReference _hotKeyRefrence;
[SerializeField] internal EHotkeyPressType _hotkeyPressType;
internal void OnHotkeyTriggered()
{
if (m_Interactable)
{
OnSubmit(null);
}
}
internal bool HasHotKeyRefrenced()
{
return _hotKeyRefrence != null;
}
#endif
[SerializeField] private UnityEvent<bool> m_OnValueChanged = new();
#endregion
#region Private Variables
#region Private Fields
[SerializeField] private SelectionState m_SelectionState = SelectionState.Normal;
private bool m_DownAndExistUI;
private bool m_IsDown;
private bool m_IsTogSelected;
private bool m_HasExitedWhileDown;
private bool _mTogSelected;
private Animator _animator;
private WaitForSeconds _waitTimeFadeDuration;
private Coroutine _resetRoutine;
private WaitForSeconds _waitFadeDuration;
private readonly Dictionary<string, int> _animTriggerIDs = new();
private readonly Dictionary<string, int> _animResetTriggerIDs = new();
private static readonly Dictionary<string, int> _animTriggerCache = new()
{
{ "Normal", Animator.StringToHash("Normal") },
{ "Highlighted", Animator.StringToHash("Highlighted") },
{ "Pressed", Animator.StringToHash("Pressed") },
{ "Selected", Animator.StringToHash("Selected") },
{ "Disabled", Animator.StringToHash("Disabled") },
};
#endregion
#region Properties
internal bool Interactable
private Animator Animator => _animator ? _animator : _animator = GetComponent<Animator>();
public bool Interactable
{
get => m_Interactable;
}
internal Animator animator
{
get
set
{
if (!_animator)
_animator = GetComponent<Animator>();
return _animator;
if (m_Interactable == value) return;
m_Interactable = value;
SetState(m_Interactable ? SelectionState.Normal : SelectionState.Disabled);
}
}
public bool IsSelected
public bool Selected
{
get => m_IsTogSelected;
get => _mTogSelected;
internal set
{
if (m_IsTogSelected == value) return;
m_IsTogSelected = value;
onValueChanged?.Invoke(m_IsTogSelected);
m_SelectionState = value ? SelectionState.Selected : SelectionState.Normal;
UpdateVisualState(m_SelectionState, false);
if (_mTogSelected == value) return;
_mTogSelected = value;
m_OnValueChanged?.Invoke(value);
SetState(value ? SelectionState.Selected : SelectionState.Normal);
}
}
@ -123,8 +108,6 @@ public class UXButton : UIBehaviour, IButton,
set => m_OnClick = value;
}
[SerializeField] private UnityEvent<bool> m_OnValueChanged = new();
public UnityEvent<bool> onValueChanged
{
get => m_OnValueChanged;
@ -138,166 +121,96 @@ public class UXButton : UIBehaviour, IButton,
protected override void Awake()
{
base.Awake();
Initlize();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (!Application.isPlaying)
{
Initlize();
}
}
#endif // if UNITY_EDITOR
protected void Initlize()
{
_waitTimeFadeDuration = new WaitForSeconds(
Mathf.Max(0.01f, m_TransitionData.colors.fadeDuration));
_animTriggerIDs.Clear();
_animResetTriggerIDs.Clear();
var triggers = m_TransitionData.animationTriggers;
AddTriggerID(triggers.normalTrigger);
AddTriggerID(triggers.highlightedTrigger);
AddTriggerID(triggers.pressedTrigger);
AddTriggerID(triggers.selectedTrigger);
AddTriggerID(triggers.disabledTrigger);
UpdateVisualState(m_SelectionState, true);
_waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_TransitionData.colors.fadeDuration));
ApplyVisualState(m_SelectionState, true);
}
protected override void OnDestroy()
{
if (_resetRoutine != null)
{
StopCoroutine(_resetRoutine);
_resetRoutine = null;
}
base.OnDestroy();
}
#endregion
#region Event Handlers
#region Pointer Handlers
void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
public void OnPointerDown(PointerEventData eventData)
{
if (!ShouldProcessEvent(eventData)) return;
if (!CanProcess(eventData)) return;
m_IsDown = true;
m_SelectionState = SelectionState.Pressed;
UpdateVisualState(m_SelectionState, false);
SetState(SelectionState.Pressed);
}
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
public void OnPointerUp(PointerEventData eventData)
{
if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left)
return;
m_IsDown = false;
if (m_IsTogSelected)
{
m_SelectionState = SelectionState.Selected;
}
else
{
m_SelectionState = m_DownAndExistUI ? SelectionState.Normal : SelectionState.Highlighted;
}
var newState = _mTogSelected
? SelectionState.Selected
: m_HasExitedWhileDown
? SelectionState.Normal
: SelectionState.Highlighted;
UpdateVisualState(m_SelectionState, false);
SetState(newState);
}
void IPointerEnterHandler.OnPointerEnter(PointerEventData eventData)
public void OnPointerEnter(PointerEventData eventData)
{
if (!ShouldProcessEvent(eventData)) return;
m_DownAndExistUI = false;
if (!CanProcess(eventData)) return;
m_HasExitedWhileDown = false;
if (m_IsDown) return;
m_SelectionState = SelectionState.Highlighted;
UpdateVisualState(m_SelectionState, false);
PlayButtonSound(hoverAudioClip);
SetState(SelectionState.Highlighted);
PlayAudio(hoverAudioClip);
}
void IPointerExitHandler.OnPointerExit(PointerEventData eventData)
public void OnPointerExit(PointerEventData eventData)
{
if (!m_Interactable) return;
if (m_IsDown)
{
m_DownAndExistUI = true;
m_HasExitedWhileDown = true;
return;
}
m_SelectionState = IsSelected ? SelectionState.Selected : SelectionState.Normal;
UpdateVisualState(m_SelectionState, false);
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
}
void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
public void OnPointerClick(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left || !m_Interactable)
return;
PlayButtonSound(clickAudioClip);
ProcessClick();
PlayAudio(clickAudioClip);
HandleClick();
}
public void OnSubmit(BaseEventData eventData)
{
UpdateVisualState(SelectionState.Pressed, false);
ProcessClick();
SetState(SelectionState.Pressed);
HandleClick();
if (_resetRoutine != null)
StopCoroutine(OnFinishSubmit());
_resetRoutine = StartCoroutine(OnFinishSubmit());
}
public bool Disable
{
get => m_Interactable;
set
{
if (m_Interactable == value) return;
m_Interactable = value;
m_SelectionState = !m_Interactable ? SelectionState.Disabled : SelectionState.Normal;
UpdateVisualState(m_SelectionState, false);
}
StopCoroutine(_resetRoutine);
_resetRoutine = StartCoroutine(ResetAfterSubmit());
}
#endregion
#region Public Methods
#region Logic
public bool Selected
{
get => IsSelected;
set
{
if (m_Mode == ButtonModeType.Toggle)
{
ProcessClick();
}
}
}
#endregion
#region Private Methods
private bool ShouldProcessEvent(PointerEventData eventData)
private bool CanProcess(PointerEventData eventData)
{
return m_Interactable &&
eventData.button == PointerEventData.InputButton.Left &&
!(m_Mode == ButtonModeType.Toggle && IsSelected);
!(m_Mode == ButtonModeType.Toggle && Selected);
}
private void ProcessClick()
private void HandleClick()
{
if (m_Mode == ButtonModeType.Normal)
{
@ -310,142 +223,87 @@ public class UXButton : UIBehaviour, IButton,
}
else
{
IsSelected = !IsSelected;
Selected = !Selected;
}
}
private void UpdateVisualState(SelectionState state, bool instant)
private void SetState(SelectionState state)
{
ProcessTransitionData(m_TransitionData, state, instant);
foreach (var transition in m_ChildTransitions)
{
ProcessTransitionData(transition, state, instant);
}
if (m_SelectionState == state) return;
m_SelectionState = state;
ApplyVisualState(state, false);
}
private void ProcessTransitionData(TransitionData transition, SelectionState state, bool instant)
private void ApplyVisualState(SelectionState state, bool instant)
{
if ((transition.transition == Selectable.Transition.ColorTint || transition.transition == Selectable.Transition.SpriteSwap) && transition.targetGraphic == null) return;
Color tintColor;
Sprite transitionSprite;
string triggerName;
switch (state)
{
case SelectionState.Normal:
tintColor = transition.colors.normalColor;
transitionSprite = transition.spriteState.highlightedSprite;
triggerName = transition.animationTriggers.normalTrigger;
break;
case SelectionState.Highlighted:
tintColor = transition.colors.highlightedColor;
transitionSprite = transition.spriteState.highlightedSprite;
triggerName = transition.animationTriggers.highlightedTrigger;
break;
case SelectionState.Pressed:
tintColor = transition.colors.pressedColor;
transitionSprite = transition.spriteState.pressedSprite;
triggerName = transition.animationTriggers.pressedTrigger;
break;
case SelectionState.Selected:
tintColor = transition.colors.selectedColor;
transitionSprite = transition.spriteState.selectedSprite;
triggerName = transition.animationTriggers.selectedTrigger;
break;
case SelectionState.Disabled:
tintColor = transition.colors.disabledColor;
transitionSprite = transition.spriteState.disabledSprite;
triggerName = transition.animationTriggers.disabledTrigger;
break;
default:
return;
}
switch (transition.transition)
{
case Selectable.Transition.ColorTint:
StartColorTween(transition, tintColor * transition.colors.colorMultiplier, instant);
break;
case Selectable.Transition.SpriteSwap:
DoSpriteSwap(transition, transitionSprite);
break;
case Selectable.Transition.Animation:
TriggerAnimation(triggerName);
break;
}
ApplyTransition(m_TransitionData, state, instant);
for (int i = 0; i < m_ChildTransitions.Count; i++)
ApplyTransition(m_ChildTransitions[i], state, instant);
}
private void StartColorTween(TransitionData data, Color targetColor, bool instant)
private void ApplyTransition(TransitionData data, SelectionState state, bool instant)
{
if (Application.isPlaying)
{
data.targetGraphic.CrossFadeColor(
targetColor,
instant ? 0f : data.colors.fadeDuration,
true,
true
);
}
else
{
data.targetGraphic.canvasRenderer.SetColor(targetColor);
}
}
private void DoSpriteSwap(TransitionData data, Sprite newSprite)
{
if (data.targetGraphic is Image image)
{
image.overrideSprite = newSprite;
}
}
private void TriggerAnimation(string trigger)
{
if (animator == null ||
!animator.isActiveAndEnabled ||
!animator.hasBoundPlayables ||
string.IsNullOrEmpty(trigger))
if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation)
return;
foreach (var resetTrigger in _animResetTriggerIDs.Keys)
(Color color, Sprite sprite, string trigger) = state switch
{
animator.ResetTrigger(_animTriggerIDs[resetTrigger]);
}
SelectionState.Normal => (data.colors.normalColor, data.spriteState.highlightedSprite, data.animationTriggers.normalTrigger),
SelectionState.Highlighted => (data.colors.highlightedColor, data.spriteState.highlightedSprite, data.animationTriggers.highlightedTrigger),
SelectionState.Pressed => (data.colors.pressedColor, data.spriteState.pressedSprite, data.animationTriggers.pressedTrigger),
SelectionState.Selected => (data.colors.selectedColor, data.spriteState.selectedSprite, data.animationTriggers.selectedTrigger),
SelectionState.Disabled => (data.colors.disabledColor, data.spriteState.disabledSprite, data.animationTriggers.disabledTrigger),
_ => (Color.white, null, null)
};
if (_animTriggerIDs.TryGetValue(trigger, out int id))
switch (data.transition)
{
animator.SetTrigger(id);
case Selectable.Transition.ColorTint:
TweenColor(data, color * data.colors.colorMultiplier, instant);
break;
case Selectable.Transition.SpriteSwap:
SwapSprite(data, sprite);
break;
case Selectable.Transition.Animation:
PlayAnimation(trigger);
break;
}
}
private void AddTriggerID(string triggerName)
private void TweenColor(TransitionData data, Color color, bool instant)
{
if (!string.IsNullOrEmpty(triggerName))
{
int id = Animator.StringToHash(triggerName);
if (!_animTriggerIDs.ContainsKey(triggerName))
{
_animTriggerIDs.Add(triggerName, id);
_animResetTriggerIDs.Add(triggerName, id);
}
}
data.targetGraphic.CrossFadeColor(color, instant ? 0f : data.colors.fadeDuration, true, true);
}
private void PlayButtonSound(AudioClip clip)
private static void SwapSprite(TransitionData data, Sprite sprite)
{
if (clip == null || UXComponentExtensionsHelper.AudioHelper == null) return;
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
if (data.targetGraphic is Image img)
img.overrideSprite = sprite;
}
private IEnumerator OnFinishSubmit()
private void PlayAnimation(string trigger)
{
yield return _waitTimeFadeDuration;
UpdateVisualState(
m_IsTogSelected ? SelectionState.Selected : SelectionState.Normal,
false
);
if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger))
return;
foreach (int id in _animTriggerCache.Values)
Animator.ResetTrigger(id);
if (_animTriggerCache.TryGetValue(trigger, out int hash))
Animator.SetTrigger(hash);
}
private void PlayAudio(AudioClip clip)
{
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
}
private IEnumerator ResetAfterSubmit()
{
yield return _waitFadeDuration;
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
_resetRoutine = null;
}

View File

@ -1,17 +1,17 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine.EventSystems;
[DisallowMultipleComponent]
public class UXGroup : UIBehaviour
{
[SerializeField] private bool m_AllowSwitchOff;
[ReadOnly, SerializeField] private List<UXButton> m_Buttons = new();
[SerializeField] private List<UXButton> m_Buttons = new();
private UXButton _current;
private readonly HashSet<UXButton> _registeredButtons = new();
private readonly HashSet<UXButton> _registered = new();
public UnityEvent<UXButton> onSelectedChanged = new();
@ -21,169 +21,118 @@ public class UXGroup : UIBehaviour
set
{
m_AllowSwitchOff = value;
ValidateGroupState();
ValidateGroup();
}
}
protected override void Awake() => ValidateGroup();
protected override void OnDestroy()
{
foreach (var button in _registeredButtons)
{
if (button) button.IsSelected = false;
}
foreach (var btn in _registered)
if (btn)
btn.Selected = false;
_registered.Clear();
m_Buttons.Clear();
_registeredButtons.Clear();
base.OnDestroy();
}
protected override void Awake() => ValidateGroupState();
public void RegisterButton(UXButton button)
{
if (!button || _registeredButtons.Contains(button)) return;
if (!button || !_registered.Add(button)) return;
if (!m_Buttons.Contains(button)) m_Buttons.Add(button);
m_Buttons.Add(button);
_registeredButtons.Add(button);
if (button.IsSelected)
if (button.Selected)
{
if (_current && _current != button)
_current.IsSelected = false;
_current.Selected = false;
_current = button;
}
ValidateGroupState();
ValidateGroup();
}
public void UnregisterButton(UXButton button)
{
if (!button || !_registeredButtons.Contains(button)) return;
if (!button || !_registered.Remove(button)) return;
m_Buttons.Remove(button);
_registeredButtons.Remove(button);
button.IsSelected = false;
if (_current == button)
_current = null;
button.Selected = false;
}
internal void NotifyButtonClicked(UXButton clickedButton)
internal void NotifyButtonClicked(UXButton button)
{
if (clickedButton.IsSelected)
if (!button) return;
if (button.Selected)
{
if (m_AllowSwitchOff)
SetSelectedButton(null);
if (m_AllowSwitchOff) SetSelected(null);
}
else
{
SetSelectedButton(clickedButton);
SetSelected(button);
}
}
private void SetSelectedButton(UXButton target)
private void SetSelected(UXButton target)
{
var previous = _current;
_current = null;
foreach (var button in m_Buttons)
foreach (var btn in m_Buttons)
{
bool shouldSelect = (button == target);
if (button.IsSelected != shouldSelect)
button.IsSelected = shouldSelect;
if (shouldSelect)
_current = button;
bool select = (btn == target);
if (btn.Selected != select)
btn.Selected = select;
if (select) _current = btn;
}
if (previous != _current)
onSelectedChanged?.Invoke(_current);
}
private void ValidateGroupState()
private void ValidateGroup()
{
bool hasSelected = _current != null && _current.IsSelected;
if (!hasSelected && m_Buttons.Count > 0 && !m_AllowSwitchOff)
SetSelectedButton(m_Buttons[0]);
if (_current != null && _current.Selected) return;
if (!m_AllowSwitchOff && m_Buttons.Count > 0)
SetSelected(m_Buttons[0]);
}
public bool AnyOtherSelected(UXButton exclusion)
public bool AnyOtherSelected(UXButton exclude)
{
foreach (var button in m_Buttons)
{
if (button != exclusion && button.IsSelected)
foreach (var btn in m_Buttons)
if (btn != exclude && btn.Selected)
return true;
}
return false;
}
public void SelectNext() => SelectRelative(1);
public void SelectPrevious() => SelectRelative(-1);
public void SelectPrevious()
private void SelectRelative(int dir)
{
if (m_Buttons.Count == 0) return;
int startIndex = _current ? m_Buttons.IndexOf(_current) : -1;
int newIndex = FindSelectableIndex(startIndex, -1);
if (newIndex != -1)
{
SetSelectedButton(m_Buttons[newIndex]);
}
int start = _current ? m_Buttons.IndexOf(_current) : -1;
int next = FindNextSelectable(start, dir);
if (next >= 0) SetSelected(m_Buttons[next]);
}
public void SelectNext()
{
if (m_Buttons.Count == 0) return;
int startIndex = _current ? m_Buttons.IndexOf(_current) : -1;
int newIndex = FindSelectableIndex(startIndex, 1);
if (newIndex != -1)
{
SetSelectedButton(m_Buttons[newIndex]);
}
}
private int FindSelectableIndex(int startIndex, int direction)
private int FindNextSelectable(int startIndex, int dir)
{
if (m_Buttons.Count == 0) return -1;
int buttonCount = m_Buttons.Count - 1;
int valueIndex = startIndex == -1 ? 0 : startIndex + direction;
int fallBackIndex = -1;
int count = m_Buttons.Count;
int index = (startIndex + dir + count) % count;
valueIndex = valueIndex > buttonCount ? 0 : (valueIndex < 0 ? buttonCount : valueIndex);
if (valueIndex > buttonCount)
for (int i = 0; i < count; i++)
{
valueIndex = 0;
}
else if (valueIndex < 0)
{
valueIndex = buttonCount;
var btn = m_Buttons[index];
if (btn && btn.isActiveAndEnabled && btn.Interactable)
return index;
index = (index + dir + count) % count;
}
while (valueIndex != startIndex)
{
UXButton btn = m_Buttons[valueIndex];
if (btn.isActiveAndEnabled && btn.Interactable)
{
fallBackIndex = valueIndex;
break;
}
valueIndex += direction;
if (valueIndex > buttonCount)
{
valueIndex = 0;
}
else if (valueIndex < 0)
{
valueIndex = buttonCount;
}
}
return fallBackIndex;
return -1;
}
}

View File

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
#if INPUTSYSTEM_SUPPORT
using AlicizaX.UI.Runtime;
using Sirenix.OdinInspector;
using UnityEngine;
namespace AlicizaX.UI.Extension.UXComponent.Hotkey
@ -22,30 +20,8 @@ namespace AlicizaX.UI.Extension.UXComponent.Hotkey
_holderObjectBase.OnWindowShowEvent -= BindHotKeys;
_holderObjectBase.OnWindowClosedEvent -= UnBindHotKeys;
}
#if UNITY_EDITOR
[InlineButton("SetHotKeyButtons")]
#endif
[SerializeField]
[HideLabel]
private UXButton[] hotButtons;
#if UNITY_EDITOR
private void SetHotKeyButtons()
{
var btns = transform.GetComponentsInChildren<UXButton>(true);
var hotBtnList = new List<UXButton>();
for (int i = 0; i < btns.Length; i++)
{
if (btns[i].HasHotKeyRefrenced())
{
hotBtnList.Add(btns[i]);
}
}
hotButtons = hotBtnList.ToArray();
}
#endif
private UXHotkey[] hotButtons;
internal void BindHotKeys()
{
@ -64,3 +40,5 @@ namespace AlicizaX.UI.Extension.UXComponent.Hotkey
}
}
}
#endif

View File

@ -1,174 +1,28 @@
#if INPUTSYSTEM_SUPPORT
using System;
using UnityEngine;
using UnityEngine.InputSystem;
using System.Collections.Generic;
using System.Collections;
internal enum EHotkeyPressType
namespace AlicizaX.UI.Extension
{
Started,
Performed
}
internal static class UXHotkeyComponent
{
private readonly struct HotkeyRegistration
public class UXHotkey : MonoBehaviour
{
public readonly EHotkeyPressType pressType;
public readonly UXButton button;
[SerializeField] private UXButton _button;
public HotkeyRegistration(UXButton btn, EHotkeyPressType pressType)
[SerializeField] internal UnityEngine.InputSystem.InputActionReference _hotKeyRefrence;
[SerializeField] internal EHotkeyPressType _hotkeyPressType;
private void OnValidate()
{
button = btn;
this.pressType = pressType;
}
}
private static readonly Dictionary<string, List<HotkeyRegistration>> _hotkeyRegistry =
new Dictionary<string, List<HotkeyRegistration>>(32);
private static readonly Dictionary<string, (System.Action<InputAction.CallbackContext> handler, InputActionReference action)> _sharedHandlers =
new Dictionary<string, (System.Action<InputAction.CallbackContext>, InputActionReference)>(32);
private static readonly Dictionary<UXButton, string> _buttonRegistrations =
new Dictionary<UXButton, string>(64);
#if UNITY_EDITOR
[UnityEditor.Callbacks.DidReloadScripts]
internal static void ClearHotkeyRegistry()
{
foreach (var key in _buttonRegistrations.Keys)
{
UnregisterHotkey(key);
_button = GetComponent<UXButton>();
}
_sharedHandlers.Clear();
_hotkeyRegistry.Clear();
_buttonRegistrations.Clear();
}
#endif
internal static void RegisterHotkey(UXButton button, InputActionReference action, EHotkeyPressType pressType)
{
if (action == null || action.action == null || button == null)
return;
string actionId = action.action.id.ToString();
HotkeyRegistration registration = new HotkeyRegistration(button, pressType);
if (!_hotkeyRegistry.TryGetValue(actionId, out var registrations))
internal void HotkeyActionTrigger()
{
registrations = new List<HotkeyRegistration>(4);
_hotkeyRegistry[actionId] = registrations;
}
registrations.Add(registration);
_buttonRegistrations[button] = actionId;
if (!_sharedHandlers.ContainsKey(actionId))
{
System.Action<InputAction.CallbackContext> handler = ctx => OnHotkeyTriggered(actionId);
_sharedHandlers[actionId] = (handler, action);
switch (pressType)
if (_button.Interactable)
{
case EHotkeyPressType.Started:
action.action.started += handler;
break;
case EHotkeyPressType.Performed:
action.action.performed += handler;
break;
}
action.action.Enable();
}
}
public static void UnregisterHotkey(UXButton button)
{
if (button == null || !_buttonRegistrations.TryGetValue(button, out var actionId))
return;
if (_hotkeyRegistry.TryGetValue(actionId, out var registrations))
{
HotkeyRegistration hotkeyInfo;
for (int i = registrations.Count - 1; i >= 0; i--)
{
if (registrations[i].button == button)
{
hotkeyInfo = registrations[i];
registrations.RemoveAt(i);
if (registrations.Count == 0 && _sharedHandlers.TryGetValue(actionId, out var handlerInfo))
{
var (handler, actionRef) = handlerInfo;
if (actionRef != null && actionRef.action != null)
{
actionRef.action.Disable();
_sharedHandlers.Remove(actionId);
_hotkeyRegistry.Remove(actionId);
switch (hotkeyInfo.pressType)
{
case EHotkeyPressType.Started:
actionRef.action.started -= handler;
break;
case EHotkeyPressType.Performed:
actionRef.action.performed -= handler;
break;
}
}
}
break;
}
_button.OnSubmit(null);
}
}
_buttonRegistrations.Remove(button);
}
private static void OnHotkeyTriggered(string actionId)
{
if (_hotkeyRegistry.TryGetValue(actionId, out var registrations) && registrations.Count > 0)
{
var registration = registrations[registrations.Count - 1];
registration.button.OnHotkeyTriggered();
}
}
}
public static class UXButtonHotkeyExtension
{
public static void BindHotKey(this UXButton button)
{
if (button == null) return;
if (button._hotKeyRefrence != null)
{
UXHotkeyComponent.RegisterHotkey(
button,
button._hotKeyRefrence,
button._hotkeyPressType
);
}
}
public static void UnBindHotKey(this UXButton button)
{
if (button == null || button._hotKeyRefrence == null) return;
UXHotkeyComponent.UnregisterHotkey(button);
}
}
#endif

View File

@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: 0aa77908962c48199d63710fa15b8c37
timeCreated: 1754555268
guid: 9aa1d0e145a6435c94190809bbc99235
timeCreated: 1760340422

View File

@ -0,0 +1,174 @@
#if INPUTSYSTEM_SUPPORT
using UnityEngine;
using UnityEngine.InputSystem;
using System.Collections.Generic;
using System.Collections;
using AlicizaX.UI.Extension;
internal enum EHotkeyPressType
{
Started,
Performed
}
internal static class UXHotkeyRegisterManager
{
private readonly struct HotkeyRegistration
{
public readonly EHotkeyPressType pressType;
public readonly UXHotkey button;
public HotkeyRegistration(UXHotkey btn, EHotkeyPressType pressType)
{
button = btn;
this.pressType = pressType;
}
}
private static readonly Dictionary<string, List<HotkeyRegistration>> _hotkeyRegistry =
new Dictionary<string, List<HotkeyRegistration>>(32);
private static readonly Dictionary<string, (System.Action<InputAction.CallbackContext> handler, InputActionReference action)> _sharedHandlers =
new Dictionary<string, (System.Action<InputAction.CallbackContext>, InputActionReference)>(32);
private static readonly Dictionary<UXHotkey, string> _buttonRegistrations =
new Dictionary<UXHotkey, string>(64);
#if UNITY_EDITOR
[UnityEditor.Callbacks.DidReloadScripts]
internal static void ClearHotkeyRegistry()
{
foreach (var key in _buttonRegistrations.Keys)
{
UnregisterHotkey(key);
}
_sharedHandlers.Clear();
_hotkeyRegistry.Clear();
_buttonRegistrations.Clear();
}
#endif
internal static void RegisterHotkey(UXHotkey button, InputActionReference action, EHotkeyPressType pressType)
{
if (action == null || action.action == null || button == null)
return;
string actionId = action.action.id.ToString();
HotkeyRegistration registration = new HotkeyRegistration(button, pressType);
if (!_hotkeyRegistry.TryGetValue(actionId, out var registrations))
{
registrations = new List<HotkeyRegistration>(4);
_hotkeyRegistry[actionId] = registrations;
}
registrations.Add(registration);
_buttonRegistrations[button] = actionId;
if (!_sharedHandlers.ContainsKey(actionId))
{
System.Action<InputAction.CallbackContext> handler = ctx => OnHotkeyTriggered(actionId);
_sharedHandlers[actionId] = (handler, action);
switch (pressType)
{
case EHotkeyPressType.Started:
action.action.started += handler;
break;
case EHotkeyPressType.Performed:
action.action.performed += handler;
break;
}
action.action.Enable();
}
}
public static void UnregisterHotkey(UXHotkey button)
{
if (button == null || !_buttonRegistrations.TryGetValue(button, out var actionId))
return;
if (_hotkeyRegistry.TryGetValue(actionId, out var registrations))
{
HotkeyRegistration hotkeyInfo;
for (int i = registrations.Count - 1; i >= 0; i--)
{
if (registrations[i].button == button)
{
hotkeyInfo = registrations[i];
registrations.RemoveAt(i);
if (registrations.Count == 0 && _sharedHandlers.TryGetValue(actionId, out var handlerInfo))
{
var (handler, actionRef) = handlerInfo;
if (actionRef != null && actionRef.action != null)
{
actionRef.action.Disable();
_sharedHandlers.Remove(actionId);
_hotkeyRegistry.Remove(actionId);
switch (hotkeyInfo.pressType)
{
case EHotkeyPressType.Started:
actionRef.action.started -= handler;
break;
case EHotkeyPressType.Performed:
actionRef.action.performed -= handler;
break;
}
}
}
break;
}
}
}
_buttonRegistrations.Remove(button);
}
private static void OnHotkeyTriggered(string actionId)
{
if (_hotkeyRegistry.TryGetValue(actionId, out var registrations) && registrations.Count > 0)
{
var registration = registrations[^1];
registration.button.HotkeyActionTrigger();
}
}
}
public static class UXHotkeyHotkeyExtension
{
public static void BindHotKey(this UXHotkey button)
{
if (button == null) return;
if (button._hotKeyRefrence != null)
{
UXHotkeyRegisterManager.RegisterHotkey(
button,
button._hotKeyRefrence,
button._hotkeyPressType
);
}
}
public static void UnBindHotKey(this UXHotkey button)
{
if (button == null || button._hotKeyRefrence == null) return;
UXHotkeyRegisterManager.UnregisterHotkey(button);
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0aa77908962c48199d63710fa15b8c37
timeCreated: 1754555268