From 551423f09d7f753de9436e9942718f79b81ff081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Fri, 19 Dec 2025 20:26:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 大优化 懒得写了 重构了分离了 UXButton UXToggle UXSelectable UXGroup --- Editor/Helper/ExtensionHelper.cs | 2 +- Editor/Helper/GUILayoutHelper.cs | 4 +- Editor/Inspector/TabbedInspector.cs | 170 +++ Editor/Inspector/TabbedInspector.cs.meta | 3 + Editor/Inspector/UXCreateHelper.cs | 18 +- Editor/Res/ComponentIcon/Toggle Icon.png | Bin 0 -> 468 bytes Editor/Res/ComponentIcon/Toggle Icon.png.meta | 127 ++ Editor/UX/Button/UButtonEditor.cs | 105 ++ Editor/UX/Button/UButtonEditor.cs.meta | 3 + Editor/UX/Button/UXButtonEditor.cs | 401 ------ Editor/UX/Button/UXButtonEditor.cs.meta | 3 - Editor/UX/Group/UXGroupEditor.cs | 270 ++++ ...nspector.cs.meta => UXGroupEditor.cs.meta} | 0 Editor/UX/Group/UXGroupInspector.cs | 153 --- .../UX/Hotkey/HotkeyBindComponentInspector.cs | 53 +- Editor/UX/Hotkey/UXHotkeyButtonEditor.cs | 34 - Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta | 3 - Editor/UX/{Iamge.meta => Image.meta} | 0 Editor/UX/{Iamge => Image}/UXImageEditor.cs | 0 .../UX/{Iamge => Image}/UXImageEditor.cs.meta | 0 Editor/UX/Selectable/UXSelectableEditor.cs | 823 ++++++----- Editor/UX/Slider.meta | 3 - Editor/UX/Slider/UXSliderEditor.cs | 158 --- Editor/UX/Slider/UXSliderEditor.cs.meta | 3 - Editor/UX/Toggle.meta | 3 + Editor/UX/Toggle/UXToggleEditor.cs | 178 +++ Editor/UX/Toggle/UXToggleEditor.cs.meta | 3 + Runtime/UXComponent/Button/UXButton.cs | 603 +-------- Runtime/UXComponent/Group/UXGroup.cs | 353 +++-- Runtime/UXComponent/Group/UXToggle.cs | 260 ++++ .../UXToggle.cs.meta} | 4 +- .../UXComponent/Hotkey/HotkeyBindComponent.cs | 49 +- Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs | 14 + .../UXComponent/Hotkey/IHotkeyTrigger.cs.meta | 3 + Runtime/UXComponent/Hotkey/UXHotkeyButton.cs | 28 - .../Hotkey/UXHotkeyRegisterManager.cs | 27 +- .../Selectable/MultipleDisplayUtilities.cs | 133 -- .../MultipleDisplayUtilities.cs.meta | 3 - .../Selectable/SetPropertyUtility.cs | 35 - .../Selectable/SetPropertyUtility.cs.meta | 3 - .../UXComponent/Selectable/UXNavigation.cs | 85 -- .../Selectable/UXNavigation.cs.meta | 3 - .../UXComponent/Selectable/UXSelectable.cs | 717 ++-------- Runtime/UXComponent/UXSlider/UXSlider.cs | 1206 ++++++++--------- 44 files changed, 2618 insertions(+), 3428 deletions(-) create mode 100644 Editor/Inspector/TabbedInspector.cs create mode 100644 Editor/Inspector/TabbedInspector.cs.meta create mode 100644 Editor/Res/ComponentIcon/Toggle Icon.png create mode 100644 Editor/Res/ComponentIcon/Toggle Icon.png.meta create mode 100644 Editor/UX/Button/UButtonEditor.cs create mode 100644 Editor/UX/Button/UButtonEditor.cs.meta delete mode 100644 Editor/UX/Button/UXButtonEditor.cs delete mode 100644 Editor/UX/Button/UXButtonEditor.cs.meta create mode 100644 Editor/UX/Group/UXGroupEditor.cs rename Editor/UX/Group/{UXGroupInspector.cs.meta => UXGroupEditor.cs.meta} (100%) delete mode 100644 Editor/UX/Group/UXGroupInspector.cs delete mode 100644 Editor/UX/Hotkey/UXHotkeyButtonEditor.cs delete mode 100644 Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta rename Editor/UX/{Iamge.meta => Image.meta} (100%) rename Editor/UX/{Iamge => Image}/UXImageEditor.cs (100%) rename Editor/UX/{Iamge => Image}/UXImageEditor.cs.meta (100%) delete mode 100644 Editor/UX/Slider.meta delete mode 100644 Editor/UX/Slider/UXSliderEditor.cs delete mode 100644 Editor/UX/Slider/UXSliderEditor.cs.meta create mode 100644 Editor/UX/Toggle.meta create mode 100644 Editor/UX/Toggle/UXToggleEditor.cs create mode 100644 Editor/UX/Toggle/UXToggleEditor.cs.meta create mode 100644 Runtime/UXComponent/Group/UXToggle.cs rename Runtime/UXComponent/{Hotkey/UXHotkeyButton.cs.meta => Group/UXToggle.cs.meta} (64%) create mode 100644 Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs create mode 100644 Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs.meta delete mode 100644 Runtime/UXComponent/Hotkey/UXHotkeyButton.cs delete mode 100644 Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs delete mode 100644 Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs.meta delete mode 100644 Runtime/UXComponent/Selectable/SetPropertyUtility.cs delete mode 100644 Runtime/UXComponent/Selectable/SetPropertyUtility.cs.meta delete mode 100644 Runtime/UXComponent/Selectable/UXNavigation.cs delete mode 100644 Runtime/UXComponent/Selectable/UXNavigation.cs.meta diff --git a/Editor/Helper/ExtensionHelper.cs b/Editor/Helper/ExtensionHelper.cs index 1a0e843..5e6522e 100644 --- a/Editor/Helper/ExtensionHelper.cs +++ b/Editor/Helper/ExtensionHelper.cs @@ -3,7 +3,7 @@ using System.Reflection; using UnityEditor; using UnityEngine; -namespace AlicizaX.UI.Extension +namespace UnityEditor.Extensions { internal static class ExtensionHelper { diff --git a/Editor/Helper/GUILayoutHelper.cs b/Editor/Helper/GUILayoutHelper.cs index c995846..beb806c 100644 --- a/Editor/Helper/GUILayoutHelper.cs +++ b/Editor/Helper/GUILayoutHelper.cs @@ -2,7 +2,7 @@ using System; using UnityEditor; using UnityEngine; -namespace AlicizaX.UI.Extension +namespace UnityEditor.DrawUtils { internal class GUILayoutHelper { @@ -39,7 +39,7 @@ namespace AlicizaX.UI.Extension } public static void DrawProperty(SerializedProperty property, GUISkin skin, string content, - Action changeCallBack, T defaultValue = default(T)) + Action changeCallBack=null, T defaultValue = default(T)) { GUILayout.BeginHorizontal(EditorStyles.helpBox); EditorGUILayout.LabelField(new GUIContent(content), skin.FindStyle("Text"), GUILayout.Width(120)); diff --git a/Editor/Inspector/TabbedInspector.cs b/Editor/Inspector/TabbedInspector.cs new file mode 100644 index 0000000..cea4d0d --- /dev/null +++ b/Editor/Inspector/TabbedInspector.cs @@ -0,0 +1,170 @@ +// TabbedInspector.cs +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace UnityEditor.Utils +{ + /// + /// 复用的 tab 管理器(非 Editor),供任意 Editor 组合使用。 + /// 用法:在 Editor 的 OnEnable 创建一个实例并 RegisterTab / AppendToTab, + /// 在 OnInspectorGUI 调用 DrawTabs()。 + /// + public class TabbedInspector + { + public class Tab + { + public string title; + public string iconName; + public List callbacks = new List(); + + public Tab(string t, string icon) + { + title = t; + iconName = icon; + } + } + + List _tabs = new List(); + int _currentTabIndex = 0; + string _prefsKey; // 用于保存每类 inspector 的选择(可选) + + public TabbedInspector(string prefsKey = null) + { + _prefsKey = prefsKey; + if (!string.IsNullOrEmpty(_prefsKey)) + { + _currentTabIndex = EditorPrefs.GetInt(_prefsKey, 0); + } + } + + public void RegisterTab(string title, string iconName, Action drawCallback) + { + if (string.IsNullOrEmpty(title)) return; + var tab = _tabs.Find(t => t.title == title); + if (tab == null) + { + tab = new Tab(title, iconName); + _tabs.Add(tab); + } + else + { + tab.iconName = iconName; + tab.callbacks.Clear(); + } + + if (drawCallback != null) + tab.callbacks.Add(drawCallback); + } + + public void AppendToTab(string title, Action drawCallback, bool last = true) + { + if (string.IsNullOrEmpty(title) || drawCallback == null) return; + var tab = _tabs.Find(t => t.title == title); + if (tab == null) + { + tab = new Tab(title, "d_DefaultAsset Icon"); + _tabs.Add(tab); + } + + if (!tab.callbacks.Contains(drawCallback)) + { + if (last) tab.callbacks.Add(drawCallback); + else tab.callbacks.Insert(0, drawCallback); + } + } + + public void RemoveCallbackFromTab(string title, Action drawCallback) + { + if (string.IsNullOrEmpty(title) || drawCallback == null) return; + var tab = _tabs.Find(t => t.title == title); + if (tab == null) return; + tab.callbacks.RemoveAll(cb => cb == drawCallback); + } + + public void UnregisterTab(string title) + { + if (string.IsNullOrEmpty(title)) return; + _tabs.RemoveAll(t => t.title == title); + if (_tabs.Count == 0) _currentTabIndex = 0; + else if (_currentTabIndex >= _tabs.Count) _currentTabIndex = Mathf.Max(0, _tabs.Count - 1); + } + + public void EnsureDefaultTab(string title, string iconName, Action defaultCallback) + { + var tab = _tabs.Find(t => t.title == title); + if (tab == null) + { + tab = new Tab(title, iconName); + _tabs.Insert(0, tab); + } + + if (!tab.callbacks.Contains(defaultCallback)) + tab.callbacks.Insert(0, defaultCallback); + } + + public void DrawTabs() + { + if (_tabs == null || _tabs.Count == 0) + { + // nothing to draw + return; + } + + EditorGUILayout.BeginHorizontal(); + + for (int i = 0; i < _tabs.Count; i++) + { + var tab = _tabs[i]; + bool isActive = (i == _currentTabIndex); + var style = new GUIStyle(EditorStyles.toolbarButton) { fixedHeight = 25, fontSize = 11, fontStyle = isActive ? FontStyle.Bold : FontStyle.Normal }; + + var icon = EditorGUIUtility.IconContent(tab.iconName)?.image; + var content = new GUIContent(icon, tab.title); + if (GUILayout.Button(content, style)) + { + _currentTabIndex = i; + SaveIndex(); + } + + if (isActive) + { + // 一条下划线表示选中 + Rect rect = GUILayoutUtility.GetLastRect(); + EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 2, rect.width, 2), new Color(0.1f, 0.5f, 0.9f)); + } + } + + EditorGUILayout.EndHorizontal(); + GUILayout.Space(4); + + // draw callbacks for current tab + var callbacks = _tabs[_currentTabIndex].callbacks; + if (callbacks != null) + { + foreach (var cb in callbacks) + { + try + { + cb?.Invoke(); + } + catch (ExitGUIException) + { + throw; + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + } + + void SaveIndex() + { + if (!string.IsNullOrEmpty(_prefsKey)) + EditorPrefs.SetInt(_prefsKey, _currentTabIndex); + } + } +} diff --git a/Editor/Inspector/TabbedInspector.cs.meta b/Editor/Inspector/TabbedInspector.cs.meta new file mode 100644 index 0000000..dde1e32 --- /dev/null +++ b/Editor/Inspector/TabbedInspector.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2033905cec9b4aaeadee445667ca163b +timeCreated: 1766127466 \ No newline at end of file diff --git a/Editor/Inspector/UXCreateHelper.cs b/Editor/Inspector/UXCreateHelper.cs index e9ec77f..28a98ff 100644 --- a/Editor/Inspector/UXCreateHelper.cs +++ b/Editor/Inspector/UXCreateHelper.cs @@ -41,19 +41,17 @@ public class UXCreateHelper : Editor } - [MenuItem("GameObject/UI/UXSlider")] - public static void CreateUXSlider(MenuCommand menuCommand) + [MenuItem("GameObject/UI/UXToggle")] + public static void CreateUXToggle(MenuCommand menuCommand) { Type MenuOptionsType = typeof(UnityEditor.UI.SliderEditor).Assembly.GetType("UnityEditor.UI.MenuOptions"); - InvokeMethod(MenuOptionsType, "AddSlider", new object[] { menuCommand }); + InvokeMethod(MenuOptionsType, "AddToggle", new object[] { menuCommand }); GameObject obj = Selection.activeGameObject; - obj.name = "UXSlider"; - DestroyImmediate(obj.GetComponent()); - var uxSlider = obj.AddComponent(); - uxSlider.fillRect = obj.transform.Find("Fill Area/Fill").GetComponent(); - var handle = obj.transform.Find("Handle Slide Area/Handle").GetComponent(); - uxSlider.handleRect = handle; - uxSlider.targetGraphic = handle.GetComponent(); + obj.name = "UXToggle"; + DestroyImmediate(obj.GetComponent()); + var uxToggle = obj.AddComponent(); + uxToggle.graphic = obj.transform.Find("Background/Checkmark").GetComponent(); + uxToggle.targetGraphic = obj.transform.Find("Background").GetComponent(); } #if TEXTMESHPRO_SUPPORT diff --git a/Editor/Res/ComponentIcon/Toggle Icon.png b/Editor/Res/ComponentIcon/Toggle Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a8f98b7ee5844e1e9a3527147582bf5db112ede7 GIT binary patch literal 468 zcmV;_0W1EAP)3cv9_QS-%#ow92MBm2$}ac!BBUS$myjdhC_oUbRS}58mXZTwQh*@R zMA#q)_Segc8Cjm^^{1ylsq~re`&+Dt{xE*Y=m?IJB*{tNolJ%jcnm#3KP+dHatzh~ z))lBpHs^m&0NwN6<_S605VlqU{B<7M$bshgzy$W3+-uHdSKt6MpWjh~B8_wa0000< KMNUMnLSTYTht + { + if (hoverAudioClip.objectReferenceValue != null) + { + PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue); + } + }); + GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () => + { + if (clickAudioClip.objectReferenceValue != null) + { + PlayAudio((AudioClip)clickAudioClip.objectReferenceValue); + } + }); + } + + private void PlayAudio(AudioClip clip) + { + if (clip != null) + { + ExtensionHelper.PreviewAudioClip(clip); + } + } + } +} diff --git a/Editor/UX/Button/UButtonEditor.cs.meta b/Editor/UX/Button/UButtonEditor.cs.meta new file mode 100644 index 0000000..0f7b720 --- /dev/null +++ b/Editor/UX/Button/UButtonEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2d771091d826488b9ff5bc2799c11756 +timeCreated: 1766125678 \ No newline at end of file diff --git a/Editor/UX/Button/UXButtonEditor.cs b/Editor/UX/Button/UXButtonEditor.cs deleted file mode 100644 index 3cd4041..0000000 --- a/Editor/UX/Button/UXButtonEditor.cs +++ /dev/null @@ -1,401 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AlicizaX.UI.Extension; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEditorInternal; -using UnityEditor.UI; -using UnityEngine; -using UnityEngine.UI; -using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType; - -[CanEditMultipleObjects] -[CustomEditor(typeof(UXButton), true)] -internal class UXButtonEditor : UXSelectableEditor -{ - private enum TabType - { - Image, - Sound, - Event - } - - private SerializedProperty m_Mode; - private SerializedProperty m_OnValueChanged; - private SerializedProperty m_OnClick; - private SerializedProperty m_UXGroup; - private SerializedProperty m_ChildTransitions; - - private ReorderableList m_ChildTransitionList; - - private static Color darkZebraEven = new Color(0.22f, 0.22f, 0.22f); - private static Color darkZebraOdd = new Color(0.27f, 0.27f, 0.27f); - - private SerializedProperty hoverAudioClip; - private SerializedProperty clickAudioClip; - - private UXButton mTarget; - - protected override void OnEnable() - { - base.OnEnable(); - mTarget = (UXButton)target; - - m_UXGroup = serializedObject.FindProperty("m_UXGroup"); - m_Mode = serializedObject.FindProperty("m_Mode"); - m_OnValueChanged = serializedObject.FindProperty("m_OnValueChanged"); - m_OnClick = serializedObject.FindProperty("m_OnClick"); - m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions"); - hoverAudioClip = serializedObject.FindProperty("hoverAudioClip"); - clickAudioClip = serializedObject.FindProperty("clickAudioClip"); - - CreateChildTransitionList(); - - // 不再 Register Image(基类已经创建),改为 Append 到 base 的 Image 页签 - AppendToTab("Image", DrawImageHead, false); - AppendToTab("Image", DrawImageBottom); - - // 另外注册独立的 Sound / Event 页签 - RegisterTab("Sound", "d_AudioSource Icon", DrawSoundTab); - RegisterTab("Event", "EventTrigger Icon", DrawEventTab); - } - - protected override void OnDisable() - { - // 移除之前追加到 Image 页签的回调,避免残留 - RemoveCallbackFromTab("Image", DrawImageHead); - UnregisterTab("Sound"); - UnregisterTab("Event"); - - base.OnDisable(); - } - - private void CreateChildTransitionList() - { - m_ChildTransitionList = new ReorderableList(serializedObject, m_ChildTransitions, true, false, true, true); - - m_ChildTransitionList.drawElementBackgroundCallback = (rect, index, isActive, isFocused) => - { - var background = index % 2 == 0 ? darkZebraEven : darkZebraOdd; - EditorGUI.DrawRect(rect, background); - }; - - m_ChildTransitionList.drawElementCallback = (rect, index, isActive, isFocused) => - { - var element = m_ChildTransitionList.serializedProperty.GetArrayElementAtIndex(index); - rect.y += 2; - - string elementTitle = $"Null Transition"; - var targetProp = element.FindPropertyRelative("targetGraphic"); - if (targetProp.objectReferenceValue != null) - elementTitle = targetProp.objectReferenceValue.name; - - EditorGUI.LabelField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), - elementTitle, EditorStyles.boldLabel); - - rect.y += EditorGUIUtility.singleLineHeight + 2; - DrawTransitionData(new Rect(rect.x, rect.y, rect.width, 0), element, true); - }; - - m_ChildTransitionList.elementHeightCallback = (index) => - { - return EditorGUIUtility.singleLineHeight + - CalculateTransitionDataHeight(m_ChildTransitionList.serializedProperty.GetArrayElementAtIndex(index)) + - 10; - }; - - m_ChildTransitionList.onAddCallback = (list) => - { - list.serializedProperty.arraySize++; - serializedObject.ApplyModifiedProperties(); - }; - - m_ChildTransitionList.onRemoveCallback = (list) => { ReorderableList.defaultBehaviours.DoRemoveButton(list); }; - } - - - // 追加到 base Image 页签的内容 - private void DrawImageHead() - { - EditorGUI.BeginDisabledGroup(EditorApplication.isPlaying); - var modeType = (ButtonModeType)EditorGUILayout.EnumPopup("Mode", (ButtonModeType)m_Mode.enumValueIndex); - if (modeType != (ButtonModeType)m_Mode.enumValueIndex) - { - if (modeType == ButtonModeType.Normal) - { - ResetEventProperty(m_OnValueChanged); - m_UXGroup.objectReferenceValue = null; - } - else - { - ResetEventProperty(m_OnClick); - } - - m_Mode.enumValueIndex = (int)modeType; - } - - EditorGUI.EndDisabledGroup(); - - - - } - - private void DrawImageBottom() - { - GUILayout.Space(5); - - DrawChildTransitions(); - - GUILayout.Space(1); - DrawBasicSettings(); - } - - private void DrawSoundTab() - { - GUILayoutHelper.DrawProperty(hoverAudioClip, customSkin, "Hover Sound", "Play", () => - { - if (hoverAudioClip.objectReferenceValue != null) - { - PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue); - } - }); - GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () => - { - if (clickAudioClip.objectReferenceValue != null) - { - PlayAudio((AudioClip)clickAudioClip.objectReferenceValue); - } - }); - } - - protected virtual void DrawEventTab() - { - if (m_Mode.enumValueIndex == (int)ButtonModeType.Toggle) - { - EditorGUILayout.Space(); - EditorGUILayout.PropertyField(m_OnValueChanged); - } - else - { - EditorGUILayout.Space(); - EditorGUILayout.PropertyField(m_OnClick); - } - - EditorGUILayout.Separator(); - } - - // helpers kept from original file - private void ResetEventProperty(SerializedProperty property) - { - SerializedProperty persistentCalls = property.FindPropertyRelative("m_PersistentCalls"); - SerializedProperty calls = persistentCalls.FindPropertyRelative("m_Calls"); - calls.arraySize = 0; - property.serializedObject.ApplyModifiedProperties(); - } - - private void PlayAudio(AudioClip clip) - { - if (clip != null) - { - ExtensionHelper.PreviewAudioClip(clip); - } - } - - private void DrawBasicSettings() - { - if (m_Mode.enumValueIndex == (int)ButtonModeType.Toggle) - { - GUILayoutHelper.DrawProperty(m_UXGroup, customSkin, "UXGroup", (oldValue, newValue) => - { - UXButton self = target as UXButton; - if (oldValue != null) - { - oldValue.UnregisterButton(self); - } - - if (newValue != null) - { - newValue.RegisterButton(self); - } - }); - } - } - - private void DrawChildTransitions() - { - m_ChildTransitionList.DoLayoutList(); - } - - // CalculateTransitionDataHeight & DrawTransitionData remain the same as earlier to preserve child-only Animation restriction and warnings. - private float CalculateTransitionDataHeight(SerializedProperty transitionData) - { - float height = 0; - SerializedProperty transition = transitionData.FindPropertyRelative("transition"); - var currentTransition = GetTransition(transition); - - bool isChild = m_ChildTransitions != null && - !string.IsNullOrEmpty(m_ChildTransitions.propertyPath) && - transitionData.propertyPath.StartsWith(m_ChildTransitions.propertyPath); - - if (isChild && currentTransition == Selectable.Transition.Animation) - { - currentTransition = Selectable.Transition.ColorTint; - } - - height += EditorGUIUtility.singleLineHeight * 1.5f; - height += EditorGUIUtility.singleLineHeight; - - SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic"); - var graphic = targetGraphic.objectReferenceValue as Graphic; - var animator = graphic != null ? graphic.GetComponent() : null; - - switch (currentTransition) - { - case Selectable.Transition.ColorTint: - if (graphic == null) height += EditorGUIUtility.singleLineHeight; - break; - case Selectable.Transition.SpriteSwap: - if (!(graphic is Image)) height += EditorGUIUtility.singleLineHeight; - break; - case Selectable.Transition.Animation: - if (animator == null) height += EditorGUIUtility.singleLineHeight; - break; - } - - switch (currentTransition) - { - case Selectable.Transition.ColorTint: - height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("colors")); - break; - case Selectable.Transition.SpriteSwap: - height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("spriteState")); - break; - case Selectable.Transition.Animation: - height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("animationTriggers")); - break; - } - - return height; - } - - private void DrawTransitionData(Rect position, SerializedProperty transitionData, bool isChild = false) - { - SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic"); - SerializedProperty transition = transitionData.FindPropertyRelative("transition"); - SerializedProperty colorBlock = transitionData.FindPropertyRelative("colors"); - SerializedProperty spriteState = transitionData.FindPropertyRelative("spriteState"); - SerializedProperty animationTriggers = transitionData.FindPropertyRelative("animationTriggers"); - - EditorGUI.indentLevel++; - float lineHeight = EditorGUIUtility.singleLineHeight; - float spacing = 2f; - float y = position.y; - - var currentTransition = GetTransition(transition); - - if (isChild && currentTransition == Selectable.Transition.Animation) - { - Rect warningRect = new Rect(position.x, y, position.width, lineHeight); - EditorGUI.HelpBox(warningRect, "Animation 过渡仅允许用于 Main Transition,子 Transition 不支持 Animation(已显示为 ColorTint)", MessageType.Warning); - y += lineHeight + spacing; - currentTransition = Selectable.Transition.ColorTint; - } - - switch (currentTransition) - { - case Selectable.Transition.ColorTint: - case Selectable.Transition.SpriteSwap: - Rect targetRect = new Rect(position.x, y, position.width, lineHeight); - EditorGUI.PropertyField(targetRect, targetGraphic); - y += lineHeight + spacing; - break; - } - - Rect transitionRect = new Rect(position.x, y, position.width, lineHeight); - - if (isChild) - { - string[] options = new[] { "None", "Color Tint", "Sprite Swap" }; - int[] values = new[] { (int)Selectable.Transition.None, (int)Selectable.Transition.ColorTint, (int)Selectable.Transition.SpriteSwap }; - - int curVal = transition.enumValueIndex; - int selIdx = Array.IndexOf(values, curVal); - if (selIdx < 0) selIdx = 0; - - selIdx = EditorGUI.Popup(transitionRect, "Transition", selIdx, options); - transition.enumValueIndex = values[selIdx]; - - currentTransition = GetTransition(transition); - y += lineHeight + spacing; - } - else - { - EditorGUI.PropertyField(transitionRect, transition); - y += lineHeight + spacing; - } - - var graphic = targetGraphic.objectReferenceValue as Graphic; - var animator = graphic != null ? graphic.GetComponent() : null; - - switch (currentTransition) - { - case Selectable.Transition.ColorTint: - if (graphic == null) - { - Rect warningRect = new Rect(position.x, y, position.width, lineHeight); - EditorGUI.HelpBox(warningRect, "需要Graphic组件来使用颜色过渡", MessageType.Warning); - y += lineHeight + spacing; - } - - break; - - case Selectable.Transition.SpriteSwap: - if (!(graphic is Image)) - { - Rect warningRect = new Rect(position.x, y, position.width, lineHeight); - EditorGUI.HelpBox(warningRect, "需要Image组件来使用精灵切换", MessageType.Warning); - y += lineHeight + spacing; - } - - break; - case Selectable.Transition.Animation: - if (animator == null) - { - Rect warningRect = new Rect(position.x, y, position.width, lineHeight); - EditorGUI.HelpBox(warningRect, "需要Animator组件来使用动画切换", MessageType.Warning); - y += lineHeight + spacing; - } - - break; - } - - switch (currentTransition) - { - case Selectable.Transition.ColorTint: - CheckAndSetColorDefaults(colorBlock, targetGraphic); - Rect colorRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(colorBlock)); - EditorGUI.PropertyField(colorRect, colorBlock); - break; - - case Selectable.Transition.SpriteSwap: - CheckAndSetColorDefaults(colorBlock, targetGraphic); - Rect spriteRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(spriteState)); - EditorGUI.PropertyField(spriteRect, spriteState); - break; - case Selectable.Transition.Animation: - Rect animRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(animationTriggers)); - EditorGUI.PropertyField(animRect, animationTriggers); - break; - } - - if (graphic != null && currentTransition != (Selectable.Transition)transition.enumValueIndex && - ((Selectable.Transition)transition.enumValueIndex == Selectable.Transition.Animation || - (Selectable.Transition)transition.enumValueIndex == Selectable.Transition.None)) - { - graphic.canvasRenderer.SetColor(Color.white); - } - - EditorGUI.indentLevel--; - } -} diff --git a/Editor/UX/Button/UXButtonEditor.cs.meta b/Editor/UX/Button/UXButtonEditor.cs.meta deleted file mode 100644 index a2c0be7..0000000 --- a/Editor/UX/Button/UXButtonEditor.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 1a6fb65845fb481293f57b30b1bfcb3b -timeCreated: 1744275051 \ No newline at end of file diff --git a/Editor/UX/Group/UXGroupEditor.cs b/Editor/UX/Group/UXGroupEditor.cs new file mode 100644 index 0000000..674eb28 --- /dev/null +++ b/Editor/UX/Group/UXGroupEditor.cs @@ -0,0 +1,270 @@ +using System.Collections.Generic; +using UnityEditorInternal; +using UnityEngine; +using UnityEngine.UI; +using UnityEditor; + +namespace UnityEditor.UI +{ + [CustomEditor(typeof(UXGroup))] + public class UXGroupInspector : Editor + { + private SerializedProperty m_Toggles; + private SerializedProperty m_AllowSwitchOff; + private SerializedProperty m_DefaultToggle; + private UXGroup _target; + private ReorderableList _reorderableList; + + private void OnEnable() + { + _target = (UXGroup)target; + m_Toggles = serializedObject.FindProperty("m_Toggles"); + m_AllowSwitchOff = serializedObject.FindProperty("m_AllowSwitchOff"); + m_DefaultToggle = serializedObject.FindProperty("m_DefaultToggle"); + + _reorderableList = new ReorderableList(serializedObject, m_Toggles, true, true, true, true) + { + drawHeaderCallback = DrawHeader, + drawElementCallback = DrawElement, + onRemoveCallback = OnRemoveList, + onChangedCallback = OnChanged, + displayAdd = false, + }; + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField(m_AllowSwitchOff); + + // Default selector: only show toggles that are currently in the group's m_Toggles list + DrawDefaultToggleSelector(); + + bool isPlaying = Application.isPlaying || EditorApplication.isPlaying; + + _reorderableList.draggable = !isPlaying; + _reorderableList.displayAdd = !isPlaying; + _reorderableList.displayRemove = !isPlaying; + + bool prevEnabled = GUI.enabled; + if (isPlaying) GUI.enabled = false; + + _reorderableList.DoLayoutList(); + + GUI.enabled = prevEnabled; + + serializedObject.ApplyModifiedProperties(); + + // 在编辑器下尽量实时同步状态 + if (!Application.isPlaying) + { + // 保证序列化数据写入后同步 group 状态 + _target.EnsureValidState(); + } + } + + private void DrawDefaultToggleSelector() + { + // Build a list of current toggles (non-null) + List toggles = new List(); + for (int i = 0; i < m_Toggles.arraySize; i++) + { + var elem = m_Toggles.GetArrayElementAtIndex(i); + var t = elem.objectReferenceValue as UXToggle; + if (t != null) + toggles.Add(t); + } + + // Prepare options array with a "None" entry + string[] options = new string[toggles.Count + 1]; + options[0] = ""; + for (int i = 0; i < toggles.Count; i++) + { + options[i + 1] = string.Format("[{0}] {1}", i, toggles[i] != null ? toggles[i].name : "Null"); + } + + // Determine current index + UXToggle currentDefault = m_DefaultToggle.objectReferenceValue as UXToggle; + int currentIndex = 0; + if (currentDefault != null) + { + int found = toggles.IndexOf(currentDefault); + if (found >= 0) + currentIndex = found + 1; // +1 because 0 is + else + { + // Current default is not in the list -> clear it + m_DefaultToggle.objectReferenceValue = null; + serializedObject.ApplyModifiedProperties(); + currentIndex = 0; + } + } + + EditorGUI.BeginChangeCheck(); + int newIndex = EditorGUILayout.Popup("Default Toggle", currentIndex, options); + if (EditorGUI.EndChangeCheck()) + { + UXToggle newDefault = null; + if (newIndex > 0) + newDefault = toggles[newIndex - 1]; + + m_DefaultToggle.objectReferenceValue = newDefault; + serializedObject.ApplyModifiedProperties(); + + // 如果选择了一个非空默认项,则在 allowSwitchOff == false 时将其设为选中状态(并通知组) + if (newDefault != null) + { + // 确保该 toggle 在 group 中(理论上应该如此) + if (!_target.ContainsToggle(newDefault)) + { + _target.RegisterToggle(newDefault); + } + + if (!_target.allowSwitchOff) + { + // 通过 SerializedObject 修改 UXToggle 的 m_IsOn,避免触发不必要的回调 + SerializedObject so = new SerializedObject(newDefault); + var isOnProp = so.FindProperty("m_IsOn"); + isOnProp.boolValue = true; + so.ApplyModifiedProperties(); + + // 通知组同步其余项 + _target.NotifyToggleOn(newDefault); + } + } + else + { + // 选择 None:不自动切换状态。但如果组不允许 all-off,则会在 EnsureValidState 中被处理 + } + } + } + + private void DrawHeader(Rect rect) + { + EditorGUI.LabelField(rect, "Toggles", EditorStyles.boldLabel); + } + + // 记录旧的引用用于侦测变化 + private UXToggle previousRef; + + private void DrawElement(Rect rect, int index, bool isActive, bool isFocused) + { + SerializedProperty element = m_Toggles.GetArrayElementAtIndex(index); + + rect.y += 2; + Rect fieldRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight); + + UXToggle oldButton = element.objectReferenceValue as UXToggle; + + string label = $"[{index}] {(oldButton != null ? oldButton.name : "Null")}"; + + EditorGUI.BeginChangeCheck(); + + var newRef = EditorGUI.ObjectField(fieldRect, label, oldButton, typeof(UXToggle), true) as UXToggle; + + if (EditorGUI.EndChangeCheck()) + { + // 先处理 Remove(旧值存在且不同) + if (oldButton != null && oldButton != newRef) + { + OnRemove(oldButton); + } + + // 再处理 Add(新值非空) + if (newRef != null && oldButton != newRef) + { + OnAdd(newRef); + } + + // 最后把引用写回去 + element.objectReferenceValue = newRef; + serializedObject.ApplyModifiedProperties(); + } + } + + private void OnAddList(ReorderableList list) + { + int newIndex = m_Toggles.arraySize; + m_Toggles.arraySize++; + serializedObject.ApplyModifiedProperties(); + + var newElem = m_Toggles.GetArrayElementAtIndex(newIndex); + newElem.objectReferenceValue = null; + serializedObject.ApplyModifiedProperties(); + } + + private void OnRemoveList(ReorderableList list) + { + if (list.index < 0 || list.index >= m_Toggles.arraySize) + return; + + var oldButton = m_Toggles.GetArrayElementAtIndex(list.index).objectReferenceValue as UXToggle; + if (oldButton) + { + OnRemove(oldButton); + } + + m_Toggles.DeleteArrayElementAtIndex(list.index); + serializedObject.ApplyModifiedProperties(); + } + + private void OnChanged(ReorderableList list) + { + serializedObject.ApplyModifiedProperties(); + + // 编辑器变动后同步 group 状态和默认项 + if (!Application.isPlaying) + _target.EnsureValidState(); + } + + // ======================== + // 自动调用的新增方法 + // ======================== + + private void OnAdd(UXToggle toggle) + { + if (toggle == null) + return; + + SerializedObject so = new SerializedObject(toggle); + var groupProp = so.FindProperty("m_Group"); + groupProp.objectReferenceValue = target; + so.ApplyModifiedProperties(); + + UXGroup group = (UXGroup)target; + group.RegisterToggle(toggle); + + // 添加后尽量同步组状态(处理默认项或冲突) + if (!Application.isPlaying) + group.EnsureValidState(); + } + + private void OnRemove(UXToggle toggle) + { + if (toggle == null) + return; + + SerializedObject so = new SerializedObject(toggle); + var groupProp = so.FindProperty("m_Group"); + + UXGroup group = groupProp.objectReferenceValue as UXGroup; + if (group != null) + group.UnregisterToggle(toggle); + + groupProp.objectReferenceValue = null; + + so.ApplyModifiedProperties(); + + // 如果移除的正好是默认项,则清空默认选项 + if (m_DefaultToggle != null && m_DefaultToggle.objectReferenceValue == toggle) + { + m_DefaultToggle.objectReferenceValue = null; + serializedObject.ApplyModifiedProperties(); + } + + if (!Application.isPlaying && _target != null) + _target.EnsureValidState(); + } + } +} diff --git a/Editor/UX/Group/UXGroupInspector.cs.meta b/Editor/UX/Group/UXGroupEditor.cs.meta similarity index 100% rename from Editor/UX/Group/UXGroupInspector.cs.meta rename to Editor/UX/Group/UXGroupEditor.cs.meta diff --git a/Editor/UX/Group/UXGroupInspector.cs b/Editor/UX/Group/UXGroupInspector.cs deleted file mode 100644 index 7f53fad..0000000 --- a/Editor/UX/Group/UXGroupInspector.cs +++ /dev/null @@ -1,153 +0,0 @@ -using AlicizaX.Editor; -using UnityEditor; -using UnityEditorInternal; -using UnityEngine; - -namespace UnityEngine.UI -{ - [CustomEditor(typeof(UXGroup))] - public class UXGroupInspector : GameFrameworkInspector - { - private SerializedProperty m_Buttons; - private UXGroup _target; - private ReorderableList _reorderableList; - - private void OnEnable() - { - _target = (UXGroup)target; - m_Buttons = serializedObject.FindProperty("m_Buttons"); - - _reorderableList = new ReorderableList(serializedObject, m_Buttons, true, true, true, true) - { - drawHeaderCallback = DrawHeader, - drawElementCallback = DrawElement, - onAddCallback = OnAddList, - onRemoveCallback = OnRemoveList, - onChangedCallback = OnChanged, - }; - } - - public override void OnInspectorGUI() - { - serializedObject.Update(); - - bool isPlaying = Application.isPlaying || EditorApplication.isPlaying; - - _reorderableList.draggable = !isPlaying; - _reorderableList.displayAdd = !isPlaying; - _reorderableList.displayRemove = !isPlaying; - - bool prevEnabled = GUI.enabled; - if (isPlaying) GUI.enabled = false; - - _reorderableList.DoLayoutList(); - - GUI.enabled = prevEnabled; - - serializedObject.ApplyModifiedProperties(); - } - - private void DrawHeader(Rect rect) - { - EditorGUI.LabelField(rect, "Toggles", EditorStyles.boldLabel); - } - - // 记录旧的引用用于侦测变化 - private UXButton previousRef; - - private void DrawElement(Rect rect, int index, bool isActive, bool isFocused) - { - SerializedProperty element = m_Buttons.GetArrayElementAtIndex(index); - - rect.y += 2; - Rect fieldRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight); - - UXButton oldButton = element.objectReferenceValue as UXButton; - - string label = $"[{index}] {(oldButton != null ? oldButton.name : "Null")}"; - - EditorGUI.BeginChangeCheck(); - - var newRef = EditorGUI.ObjectField(fieldRect, label, oldButton, typeof(UXButton), true) as UXButton; - - if (EditorGUI.EndChangeCheck()) - { - // 先处理 Remove(旧值存在且不同) - if (oldButton != null && oldButton != newRef) - { - OnRemove(oldButton); - } - - // 再处理 Add(新值非空) - if (newRef != null && oldButton != newRef) - { - OnAdd(newRef); - } - - // 最后把引用写回去 - element.objectReferenceValue = newRef; - } - } - - private void OnAddList(ReorderableList list) - { - int newIndex = m_Buttons.arraySize; - m_Buttons.arraySize++; - serializedObject.ApplyModifiedProperties(); - - var newElem = m_Buttons.GetArrayElementAtIndex(newIndex); - newElem.objectReferenceValue = null; - serializedObject.ApplyModifiedProperties(); - } - - private void OnRemoveList(ReorderableList list) - { - if (list.index < 0 || list.index >= m_Buttons.arraySize) - return; - - var oldButton = m_Buttons.GetArrayElementAtIndex(list.index).objectReferenceValue as UXButton; - if (oldButton) - { - OnRemove(oldButton); - } - - m_Buttons.DeleteArrayElementAtIndex(list.index); - serializedObject.ApplyModifiedProperties(); - } - - private void OnChanged(ReorderableList list) - { - serializedObject.ApplyModifiedProperties(); - } - - // ======================== - // 你的新增方法:自动调用 - // ======================== - - private void OnAdd(UXButton button) - { - SerializedObject so = new SerializedObject(button); - var groupProp = so.FindProperty("m_UXGroup"); - groupProp.objectReferenceValue = target; - - UXGroup group = (UXGroup)target; - group.RegisterButton(button); - - so.ApplyModifiedProperties(); - } - - private void OnRemove(UXButton button) - { - SerializedObject so = new SerializedObject(button); - var groupProp = so.FindProperty("m_UXGroup"); - - UXGroup group = groupProp.objectReferenceValue as UXGroup; - if (group != null) - group.UnregisterButton(button); - - groupProp.objectReferenceValue = null; - - so.ApplyModifiedProperties(); - } - } -} diff --git a/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs b/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs index 8be68b4..4969b32 100644 --- a/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs +++ b/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs @@ -1,13 +1,11 @@ #if INPUTSYSTEM_SUPPORT -using System.Collections.Generic; +using System.Linq; using System.Reflection; -using AlicizaX.Editor; -using AlicizaX.UI; using AlicizaX.UI.Runtime; -using UnityEditor; using UnityEngine; +using UnityEngine.UI; -namespace AlicizaX.UI111 +namespace UnityEditor.UI { [CustomEditor(typeof(HotkeyBindComponent))] public class HotkeyBindComponentInspector : UnityEditor.Editor @@ -25,7 +23,6 @@ namespace AlicizaX.UI111 { serializedObject.Update(); - var holder = _target.GetComponent(); if (holder == null) { @@ -37,14 +34,14 @@ namespace AlicizaX.UI111 EditorGUILayout.Space(); - // 🔸 灰掉显示 hotButtons 列表(不可手动编辑) + // 灰掉显示 hotButtons 列表(不可手动编辑) GUI.enabled = false; DrawHotButtonListAlwaysExpanded(hotButtonsProp); GUI.enabled = true; EditorGUILayout.Space(); - // 🔘 按钮:扫描子物体(包含隐藏) + // 按钮:扫描子物体(包含隐藏) if (GUILayout.Button("🔍 扫描所有子物体 (包含隐藏对象)")) { FindAllUXHotkeys(); @@ -60,7 +57,7 @@ namespace AlicizaX.UI111 { EditorGUILayout.LabelField("Hot Buttons", EditorStyles.boldLabel); - if (listProp.arraySize == 0) + if (listProp == null || listProp.arraySize == 0) { EditorGUILayout.HelpBox("当前没有绑定任何 UXHotkey。", MessageType.Info); return; @@ -70,11 +67,11 @@ namespace AlicizaX.UI111 for (int i = 0; i < listProp.arraySize; i++) { var element = listProp.GetArrayElementAtIndex(i); - var uxHotkey = element.objectReferenceValue as UXHotkeyButton; - string name = uxHotkey != null ? uxHotkey.name : "Null"; - EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXHotkeyButton), true); + 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--; } @@ -85,28 +82,18 @@ namespace AlicizaX.UI111 { Undo.RecordObject(_target, "Scan UXHotkey"); - var uxHotkeys = _target.GetComponentsInChildren(true); - List valiedHotkeys = new List(); - 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()); - var collectMethod = target.GetType().GetMethod("CollectUXHotkeys", BindingFlags.NonPublic | BindingFlags.Instance); - collectMethod.Invoke(target, null); - - EditorUtility.SetDirty(_target); - serializedObject.Update(); - Debug.Log($"[HotkeyBindComponent] 已找到 {uxHotkeys.Length} 个 UXHotkey 组件并绑定。"); + if (collectMethod != null) + { + collectMethod.Invoke(target, null); + EditorUtility.SetDirty(_target); + serializedObject.Update(); + } + else + { + Debug.LogWarning("未找到 CollectUXHotkeys 方法。"); + } } } } - #endif diff --git a/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs b/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs deleted file mode 100644 index 4b686a4..0000000 --- a/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs +++ /dev/null @@ -1,34 +0,0 @@ -#if INPUTSYSTEM_SUPPORT -using AlicizaX.UI; -using UnityEditor; -using UnityEngine; - -namespace AlicizaX.UI -{ - [CanEditMultipleObjects] - [CustomEditor(typeof(UXHotkeyButton), true)] - internal class UXHotkeyButtonEditor : UXButtonEditor - { - private SerializedProperty _hotKeyRefrence; - private SerializedProperty _hotkeyPressType; - - protected override void OnEnable() - { - base.OnEnable(); - _hotKeyRefrence = serializedObject.FindProperty("_hotKeyRefrence"); - _hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType"); - } - - protected override void DrawEventTab() - { - base.DrawEventTab(); - using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) - { - EditorGUILayout.PropertyField(_hotKeyRefrence, new GUIContent("InputAction")); - EditorGUILayout.PropertyField(_hotkeyPressType, new GUIContent("PressType")); - } - } - } -} - -#endif diff --git a/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta b/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta deleted file mode 100644 index aeb8405..0000000 --- a/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: dc0b010e4f884f6992de90c5e5dd7154 -timeCreated: 1765185454 \ No newline at end of file diff --git a/Editor/UX/Iamge.meta b/Editor/UX/Image.meta similarity index 100% rename from Editor/UX/Iamge.meta rename to Editor/UX/Image.meta diff --git a/Editor/UX/Iamge/UXImageEditor.cs b/Editor/UX/Image/UXImageEditor.cs similarity index 100% rename from Editor/UX/Iamge/UXImageEditor.cs rename to Editor/UX/Image/UXImageEditor.cs diff --git a/Editor/UX/Iamge/UXImageEditor.cs.meta b/Editor/UX/Image/UXImageEditor.cs.meta similarity index 100% rename from Editor/UX/Iamge/UXImageEditor.cs.meta rename to Editor/UX/Image/UXImageEditor.cs.meta diff --git a/Editor/UX/Selectable/UXSelectableEditor.cs b/Editor/UX/Selectable/UXSelectableEditor.cs index 8931fe7..db366b6 100644 --- a/Editor/UX/Selectable/UXSelectableEditor.cs +++ b/Editor/UX/Selectable/UXSelectableEditor.cs @@ -1,410 +1,344 @@ using System; using System.Collections.Generic; -using System.Linq; -using AlicizaX.UI.Extension; -using UnityEditor; -using UnityEditor.SceneManagement; +using System.Text; +using UnityEditor.AnimatedValues; +using UnityEditor.Animations; +using UnityEditor.DrawUtils; +using UnityEditor.Utils; +using UnityEditorInternal; using UnityEngine; using UnityEngine.UI; +using AnimatorController = UnityEditor.Animations.AnimatorController; using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType; -namespace UnityEngine.UI +namespace UnityEditor.UI { [CanEditMultipleObjects] [CustomEditor(typeof(UXSelectable), true)] - internal class UXSelectableEditor : Editor + internal class UXSelectableEditor : SelectableEditor { - private static bool s_ShowNavigation = false; - public static string s_ShowNavigationKey = "SelectableEditor.ShowNavigation"; - GUIContent m_VisualizeNavigation = EditorGUIUtility.TrTextContent("Visualize", "Show navigation flows between selectable UI elements."); - private static List s_Editors = new List(); + private SerializedProperty m_ChildTransitions; + private ReorderableList m_ChildTransitionList; + public TabbedInspector _tabs; - // 序列化属性(基类需要) - protected SerializedProperty m_Navigation; - protected SerializedProperty m_MainTransition; - protected SerializedProperty m_Interactable; - protected SerializedProperty m_animator; + SerializedProperty m_ChildInteractableProperty; + SerializedProperty m_ChildTargetGraphicProperty; + SerializedProperty m_ChildTransitionProperty; + SerializedProperty m_ChildColorBlockProperty; + SerializedProperty m_ChildSpriteStateProperty; + SerializedProperty m_ChildAnimTriggerProperty; + SerializedProperty m_ChildNavigationProperty; + + AnimBool m_ChildShowColorTint = new AnimBool(); + AnimBool m_ChildShowSpriteTrasition = new AnimBool(); + AnimBool m_ChildShowAnimTransition = new AnimBool(); - // 抽象皮肤(现在放在基类) protected GUISkin customSkin; - // Tab 管理结构 - protected class EditorTab + GUIContent m_ChildVisualizeNavigation = EditorGUIUtility.TrTextContent("Visualize", "Show navigation flows between selectable UI elements."); + private static bool s_ChildShowNavigation = false; + private static string s_ChildShowNavigationKey = "SelectableEditor.ShowNavigation"; + + private static Color darkZebraEven = new Color(0.22f, 0.22f, 0.22f); + private static Color darkZebraOdd = new Color(0.27f, 0.27f, 0.27f); + + protected override void OnEnable() { - public string title; - public string iconName; - public List callbacks = new List(); + base.OnEnable(); - public EditorTab(string t, string icon) - { - title = t; - iconName = icon; - } - } + m_ChildInteractableProperty = serializedObject.FindProperty("m_Interactable"); + m_ChildTargetGraphicProperty = serializedObject.FindProperty("m_TargetGraphic"); + m_ChildTransitionProperty = serializedObject.FindProperty("m_Transition"); + m_ChildColorBlockProperty = serializedObject.FindProperty("m_Colors"); + m_ChildSpriteStateProperty = serializedObject.FindProperty("m_SpriteState"); + m_ChildAnimTriggerProperty = serializedObject.FindProperty("m_AnimationTriggers"); + m_ChildNavigationProperty = serializedObject.FindProperty("m_Navigation"); + m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions"); - private List _tabs = new List(); - private int _currentTabIndex = 0; + CreateChildTransitionList(); - protected virtual void OnEnable() - { - s_ShowNavigation = EditorPrefs.GetBool(s_ShowNavigationKey); - s_Editors.Add(this); - RegisterStaticOnSceneGUI(); + m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions"); + + var trans = GetTransition(m_ChildTransitionProperty); + m_ChildShowColorTint.value = (trans == Selectable.Transition.ColorTint); + m_ChildShowSpriteTrasition.value = (trans == Selectable.Transition.SpriteSwap); + m_ChildShowAnimTransition.value = (trans == Selectable.Transition.Animation); + + m_ChildShowColorTint.valueChanged.AddListener(Repaint); + m_ChildShowSpriteTrasition.valueChanged.AddListener(Repaint); + + _tabs = new TabbedInspector(typeof(UXSelectable).FullName + ".TabbedIndex"); + _tabs.EnsureDefaultTab("Image", "d_Texture Icon", DrawBaseButtonInspector); - m_Navigation = serializedObject.FindProperty("m_Navigation"); - m_MainTransition = serializedObject.FindProperty("m_MainTransition"); - m_Interactable = serializedObject.FindProperty("m_Interactable"); - m_animator = serializedObject.FindProperty("_animator"); - // load customSkin once in base; 调整路径为你项目中 GUISkin 的实际路径(必要时修改) customSkin = AssetDatabase.LoadAssetAtPath("Packages/com.alicizax.unity.ui.extension/Editor/Res/GUISkin/UIExtensionGUISkin.guiskin"); - - // Ensure default Image tab exists and contains DrawSelectableInspector - EnsureDefaultImageTab(); } - protected virtual void OnDisable() + protected override void OnDisable() { - s_Editors.Remove(this); - RegisterStaticOnSceneGUI(); - _tabs.Clear(); - _currentTabIndex = 0; + base.OnDisable(); + m_ChildShowColorTint.valueChanged.RemoveListener(Repaint); + m_ChildShowSpriteTrasition.valueChanged.RemoveListener(Repaint); + _tabs.UnregisterTab("Image"); } - #region Tab API (Register / Append / Remove / Unregister) - - /// - /// 注册一个新 tab(如果已存在同名 tab,则替换其 icon 和清空回调,再加入 drawCallback) - /// - protected void RegisterTab(string title, string iconName, Action drawCallback) + public override void OnInspectorGUI() { - if (string.IsNullOrEmpty(title)) return; - var tab = _tabs.Find(t => t.title == title); - if (tab == null) - { - tab = new EditorTab(title, iconName); - _tabs.Add(tab); - } - else - { - tab.iconName = iconName; - tab.callbacks.Clear(); - } - - if (drawCallback != null) - tab.callbacks.Add(drawCallback); + serializedObject.Update(); + _tabs.DrawTabs(); + serializedObject.ApplyModifiedProperties(); } - /// - /// 在已存在 tab 后追加一个 callback;若 tab 不存在则创建并追加。 - /// - protected void AppendToTab(string title, Action drawCallback, bool last = true) + void DrawBaseButtonInspector() { - if (string.IsNullOrEmpty(title) || drawCallback == null) return; - var tab = _tabs.Find(t => t.title == title); - if (tab == null) + serializedObject.Update(); + + var interactable = GUILayoutHelper.DrawToggle(m_ChildInteractableProperty.boolValue, customSkin, "Interactable"); + if (interactable != m_ChildInteractableProperty.boolValue) { - tab = new EditorTab(title, "d_DefaultAsset Icon"); - _tabs.Add(tab); + m_ChildInteractableProperty.boolValue = interactable; } - // 避免重复追加(简单判断) - if (!tab.callbacks.Contains(drawCallback)) - { - if (last) - { - tab.callbacks.Add(drawCallback); - } - else - { - tab.callbacks.Insert(0, drawCallback); - } - } - } - /// - /// 从指定 tab 中移除某个 callback(子类在 OnDisable 应该调用以清理) - /// - protected void RemoveCallbackFromTab(string title, Action drawCallback) - { - if (string.IsNullOrEmpty(title) || drawCallback == null) return; - var tab = _tabs.Find(t => t.title == title); - if (tab == null) return; - tab.callbacks.RemoveAll(cb => cb == drawCallback); - } + EditorGUILayout.PropertyField(m_ChildNavigationProperty); - /// - /// 卸载整个 tab(移除该 title 的所有 callbacks 与 tab 本身)。 - /// 如果当前选中的索引超出范围,会自动修正为合法值。 - /// 注意:基类默认创建的 "Image" 页签通常不应被移除,若需要保护可以在此加判断。 - /// - protected void UnregisterTab(string title) - { - if (string.IsNullOrEmpty(title)) return; - _tabs.RemoveAll(t => t.title == title); - // 修正当前索引,避免越界 - if (_tabs.Count == 0) - { - _currentTabIndex = 0; - } - else if (_currentTabIndex >= _tabs.Count) - { - _currentTabIndex = Mathf.Max(0, _tabs.Count - 1); - } - } - - private void EnsureDefaultImageTab() - { - // 如果还没有 Image tab,创建并把 DrawSelectableInspector 放到第一个 callback - var tab = _tabs.Find(t => t.title == "Image"); - if (tab == null) - { - tab = new EditorTab("Image", "d_Texture Icon"); - _tabs.Insert(0, tab); - } - - // 确保基类绘制在 Image 页签的第一个 callback (避免重复) - if (!tab.callbacks.Contains(DrawSelectableInspector)) - tab.callbacks.Insert(0, DrawSelectableInspector); - } - - #endregion - - protected void DrawToggleShowNavigation() - { + EditorGUI.BeginChangeCheck(); Rect toggleRect = EditorGUILayout.GetControlRect(); toggleRect.xMin += EditorGUIUtility.labelWidth; - EditorGUI.BeginChangeCheck(); - s_ShowNavigation = GUI.Toggle(toggleRect, s_ShowNavigation, m_VisualizeNavigation, EditorStyles.miniButton); + s_ChildShowNavigation = GUI.Toggle(toggleRect, s_ChildShowNavigation, m_ChildVisualizeNavigation, EditorStyles.miniButton); if (EditorGUI.EndChangeCheck()) { - EditorPrefs.SetBool(s_ShowNavigationKey, s_ShowNavigation); + EditorPrefs.SetBool(s_ChildShowNavigationKey, s_ChildShowNavigation); SceneView.RepaintAll(); } - } - - #region Scene GUI visualization (unchanged) - - private void RegisterStaticOnSceneGUI() - { - SceneView.duringSceneGui -= StaticOnSceneGUI; - if (s_Editors.Count > 0) - SceneView.duringSceneGui += StaticOnSceneGUI; - } - - private static void StaticOnSceneGUI(SceneView view) - { - if (!s_ShowNavigation) - return; - - UXSelectable[] selectables = UXSelectable.allSelectablesArray; - - for (int i = 0; i < selectables.Length; i++) - { - UXSelectable s = selectables[i]; - if (StageUtility.IsGameObjectRenderedByCamera(s.gameObject, Camera.current)) - DrawNavigationForSelectable(s); - } - } - - private static void DrawNavigationForSelectable(UXSelectable sel) - { - if (sel == null) - return; - - Transform transform = sel.transform; - bool active = Selection.transforms.Any(e => e == transform); - - Handles.color = new Color(1.0f, 0.6f, 0.2f, active ? 1.0f : 0.4f); - DrawNavigationArrow(-Vector2.right, sel, sel.FindSelectableOnLeft()); - DrawNavigationArrow(Vector2.up, sel, sel.FindSelectableOnUp()); - - Handles.color = new Color(1.0f, 0.9f, 0.1f, active ? 1.0f : 0.4f); - DrawNavigationArrow(Vector2.right, sel, sel.FindSelectableOnRight()); - DrawNavigationArrow(-Vector2.up, sel, sel.FindSelectableOnDown()); - } - - const float kArrowThickness = 2.5f; - const float kArrowHeadSize = 1.2f; - - private static void DrawNavigationArrow(Vector2 direction, UXSelectable fromObj, UXSelectable toObj) - { - if (fromObj == null || toObj == null) - return; - Transform fromTransform = fromObj.transform; - Transform toTransform = toObj.transform; - - Vector2 sideDir = new Vector2(direction.y, -direction.x); - Vector3 fromPoint = fromTransform.TransformPoint(GetPointOnRectEdge(fromTransform as RectTransform, direction)); - Vector3 toPoint = toTransform.TransformPoint(GetPointOnRectEdge(toTransform as RectTransform, -direction)); - float fromSize = HandleUtility.GetHandleSize(fromPoint) * 0.05f; - float toSize = HandleUtility.GetHandleSize(toPoint) * 0.05f; - fromPoint += fromTransform.TransformDirection(sideDir) * fromSize; - toPoint += toTransform.TransformDirection(sideDir) * toSize; - float length = Vector3.Distance(fromPoint, toPoint); - Vector3 fromTangent = fromTransform.rotation * direction * length * 0.3f; - Vector3 toTangent = toTransform.rotation * -direction * length * 0.3f; - - Handles.DrawBezier(fromPoint, toPoint, fromPoint + fromTangent, toPoint + toTangent, Handles.color, null, kArrowThickness); - Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction - sideDir) * toSize * kArrowHeadSize); - Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction + sideDir) * toSize * kArrowHeadSize); - } - - private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir) - { - if (rect == null) - return Vector3.zero; - if (dir != Vector2.zero) - dir /= Mathf.Max(Mathf.Abs(dir.x), Mathf.Abs(dir.y)); - dir = rect.rect.center + Vector2.Scale(rect.rect.size, dir * 0.5f); - return dir; - } - - #endregion - - /// - /// 子类调用:绘制基类的通用 Inspector(Navigation / Interactable / Main Transition) - /// - protected void DrawSelectableInspector() - { - if (m_Navigation != null) - { - var modeProp = m_Navigation.FindPropertyRelative("m_Mode"); - - EditorGUI.BeginChangeCheck(); - EditorGUI.showMixedValue = modeProp.hasMultipleDifferentValues; - - UXNavigation.Mode cur = (UXNavigation.Mode)modeProp.intValue; - UXNavigation.Mode next = (UXNavigation.Mode)EditorGUILayout.EnumFlagsField("Navigation", cur); - - EditorGUI.showMixedValue = false; - if (EditorGUI.EndChangeCheck()) - { - modeProp.intValue = (int)next; - } - - int explicitMask = (int)UXNavigation.Mode.Explicit; - int value = modeProp.intValue; - bool onlyExplicit = !modeProp.hasMultipleDifferentValues && (value & explicitMask) == explicitMask && (value & ~explicitMask) == 0; - - if (onlyExplicit) - { - EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_SelectOnUp")); - EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_SelectOnDown")); - EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_SelectOnLeft")); - EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_SelectOnRight")); - } - - serializedObject.ApplyModifiedProperties(); - } - - - DrawToggleShowNavigation(); - - if (customSkin != null) - { - var interactable = GUILayoutHelper.DrawToggle(m_Interactable.boolValue, customSkin, "Interactable"); - if (interactable != m_Interactable.boolValue) - { - (target as UXSelectable).Interactable = interactable; - m_Interactable.boolValue = interactable; - var selProp = serializedObject.FindProperty("m_SelectionState"); - if (selProp != null) - { - selProp.enumValueIndex = interactable ? (int)UXSelectable.SelectionState.Normal : (int)UXSelectable.SelectionState.Disabled; - } - } - } GUILayout.Space(1); - serializedObject.ApplyModifiedProperties(); - DrawSelfTransition(); - GUILayout.Space(5); - } - - #region MainTransition helpers (moved to base) - - protected virtual void DrawSelfTransition() - { - if (m_MainTransition == null) - return; - GUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("Main Transition", EditorStyles.boldLabel); - SerializedProperty targetGraphic = m_MainTransition.FindPropertyRelative("targetGraphic"); - var graphic = targetGraphic.objectReferenceValue as Graphic; + var trans = GetTransition(m_ChildTransitionProperty); + + var graphic = m_ChildTargetGraphicProperty.objectReferenceValue as Graphic; if (graphic == null) + graphic = (target as Selectable).GetComponent(); + + var animator = (target as Selectable).GetComponent(); + m_ChildShowColorTint.target = (!m_ChildTransitionProperty.hasMultipleDifferentValues && trans == Button.Transition.ColorTint); + m_ChildShowSpriteTrasition.target = (!m_ChildTransitionProperty.hasMultipleDifferentValues && trans == Button.Transition.SpriteSwap); + m_ChildShowAnimTransition.target = (!m_ChildTransitionProperty.hasMultipleDifferentValues && trans == Button.Transition.Animation); + + EditorGUILayout.PropertyField(m_ChildTransitionProperty); + + { - graphic = (target as UXSelectable).GetComponent(); - if (targetGraphic != null) - targetGraphic.objectReferenceValue = graphic; + if (trans == Selectable.Transition.ColorTint || trans == Selectable.Transition.SpriteSwap) + { + EditorGUILayout.PropertyField(m_ChildTargetGraphicProperty); + } + + switch (trans) + { + case Selectable.Transition.ColorTint: + if (graphic == null) + EditorGUILayout.HelpBox("You must have a Graphic target in order to use a color transition.", MessageType.Warning); + break; + + case Selectable.Transition.SpriteSwap: + if (graphic as Image == null) + EditorGUILayout.HelpBox("You must have a Image target in order to use a sprite swap transition.", MessageType.Warning); + break; + } + + if (EditorGUILayout.BeginFadeGroup(m_ChildShowColorTint.faded)) + { + EditorGUILayout.PropertyField(m_ChildColorBlockProperty); + } + + EditorGUILayout.EndFadeGroup(); + + if (EditorGUILayout.BeginFadeGroup(m_ChildShowSpriteTrasition.faded)) + { + EditorGUILayout.PropertyField(m_ChildSpriteStateProperty); + } + + EditorGUILayout.EndFadeGroup(); + + if (EditorGUILayout.BeginFadeGroup(m_ChildShowAnimTransition.faded)) + { + EditorGUILayout.PropertyField(m_ChildAnimTriggerProperty); + + if (animator == null || animator.runtimeAnimatorController == null) + { + Rect buttonRect = EditorGUILayout.GetControlRect(); + buttonRect.xMin += EditorGUIUtility.labelWidth; + if (GUI.Button(buttonRect, "Auto Generate Animation", EditorStyles.miniButton)) + { + var controller = GenerateSelectableAnimatorContoller((target as Selectable).animationTriggers, target as Selectable); + if (controller != null) + { + if (animator == null) + animator = (target as Selectable).gameObject.AddComponent(); + + Animations.AnimatorController.SetAnimatorController(animator, controller); + } + } + } + } + + EditorGUILayout.EndFadeGroup(); } - SerializedProperty transition = m_MainTransition.FindPropertyRelative("transition"); + + EditorGUILayout.Space(); + + GUILayout.EndVertical(); + GUILayout.Space(5); + + m_ChildTransitionList.DoLayoutList(); + + GUILayout.Space(1); + serializedObject.ApplyModifiedProperties(); + } + + #region ChildTransition + + private void CreateChildTransitionList() + { + m_ChildTransitionList = new ReorderableList(serializedObject, m_ChildTransitions, true, false, true, true); + + m_ChildTransitionList.drawElementBackgroundCallback = (rect, index, isActive, isFocused) => + { + var background = index % 2 == 0 ? darkZebraEven : darkZebraOdd; + EditorGUI.DrawRect(rect, background); + }; + + m_ChildTransitionList.drawElementCallback = (rect, index, isActive, isFocused) => + { + var element = m_ChildTransitionList.serializedProperty.GetArrayElementAtIndex(index); + rect.y += 2; + + string elementTitle = $"Null Transition"; + var targetProp = element.FindPropertyRelative("targetGraphic"); + if (targetProp.objectReferenceValue != null) + elementTitle = targetProp.objectReferenceValue.name; + + EditorGUI.LabelField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), + elementTitle, EditorStyles.boldLabel); + + rect.y += EditorGUIUtility.singleLineHeight + 2; + DrawTransitionData(new Rect(rect.x, rect.y, rect.width, 0), element); + }; + + m_ChildTransitionList.elementHeightCallback = (index) => + { + return EditorGUIUtility.singleLineHeight + + CalculateTransitionDataHeight(m_ChildTransitionList.serializedProperty.GetArrayElementAtIndex(index)) + + 10; + }; + + m_ChildTransitionList.onAddCallback = (list) => + { + list.serializedProperty.arraySize++; + serializedObject.ApplyModifiedProperties(); + }; + + m_ChildTransitionList.onRemoveCallback = (list) => { ReorderableList.defaultBehaviours.DoRemoveButton(list); }; + } + + private void DrawTransitionData(Rect position, SerializedProperty transitionData) + { + SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic"); + SerializedProperty transition = transitionData.FindPropertyRelative("transition"); + SerializedProperty colorBlock = transitionData.FindPropertyRelative("colors"); + SerializedProperty spriteState = transitionData.FindPropertyRelative("spriteState"); + + EditorGUI.indentLevel++; + float lineHeight = EditorGUIUtility.singleLineHeight; + float spacing = 2f; + float y = position.y; + var currentTransition = GetTransition(transition); + if (currentTransition == Selectable.Transition.Animation) + { + Rect warningRect = new Rect(position.x, y, position.width, lineHeight); + EditorGUI.HelpBox(warningRect, "Animation 过渡仅允许用于 Main Transition,子 Transition 不支持 Animation(已显示为 ColorTint)", MessageType.Warning); + y += lineHeight + spacing; + currentTransition = Selectable.Transition.ColorTint; + } + switch (currentTransition) { case Selectable.Transition.ColorTint: case Selectable.Transition.SpriteSwap: - EditorGUILayout.PropertyField(targetGraphic); + Rect targetRect = new Rect(position.x, y, position.width, lineHeight); + EditorGUI.PropertyField(targetRect, targetGraphic); + y += lineHeight + spacing; break; } - EditorGUILayout.PropertyField(transition); + Rect transitionRect = new Rect(position.x, y, position.width, lineHeight); - Animator animator = null; - if (target is UXSelectable anima) - { - animator = anima.GetComponent(); - } - else if (m_animator.objectReferenceValue != null) - { - animator = (Animator)m_animator.objectReferenceValue; - } + string[] options = new[] { "None", "Color Tint", "Sprite Swap" }; + int[] values = new[] { (int)Selectable.Transition.None, (int)Selectable.Transition.ColorTint, (int)Selectable.Transition.SpriteSwap }; + + int curVal = transition.enumValueIndex; + int selIdx = Array.IndexOf(values, curVal); + if (selIdx < 0) selIdx = 0; + + selIdx = EditorGUI.Popup(transitionRect, "Transition", selIdx, options); + transition.enumValueIndex = values[selIdx]; + + currentTransition = GetTransition(transition); + y += lineHeight + spacing; + + var graphic = targetGraphic.objectReferenceValue as Graphic; switch (currentTransition) { case Selectable.Transition.ColorTint: if (graphic == null) - EditorGUILayout.HelpBox("需要Graphic组件来使用颜色过渡", MessageType.Warning); + { + Rect warningRect = new Rect(position.x, y, position.width, lineHeight); + EditorGUI.HelpBox(warningRect, "需要Graphic组件来使用颜色过渡", MessageType.Warning); + y += lineHeight + spacing; + } + break; + case Selectable.Transition.SpriteSwap: if (!(graphic is Image)) - EditorGUILayout.HelpBox("需要Image组件来使用精灵切换", MessageType.Warning); - break; - case Selectable.Transition.Animation: - if (animator == null) - EditorGUILayout.HelpBox("需要Animator组件来使用动画切换", MessageType.Warning); + { + Rect warningRect = new Rect(position.x, y, position.width, lineHeight); + EditorGUI.HelpBox(warningRect, "需要Image组件来使用精灵切换", MessageType.Warning); + y += lineHeight + spacing; + } + break; } switch (currentTransition) { case Selectable.Transition.ColorTint: - CheckAndSetColorDefaults(m_MainTransition.FindPropertyRelative("colors"), targetGraphic); - EditorGUILayout.PropertyField(m_MainTransition.FindPropertyRelative("colors")); - break; - case Selectable.Transition.SpriteSwap: - EditorGUILayout.PropertyField(m_MainTransition.FindPropertyRelative("spriteState")); - break; - case Selectable.Transition.Animation: - EditorGUILayout.PropertyField(m_MainTransition.FindPropertyRelative("animationTriggers")); - if (animator == null || animator.runtimeAnimatorController == null) - { - if (GUILayout.Button("Auto Generate Animation")) - { - var animationTrigger = m_MainTransition.FindPropertyRelative("animationTriggers"); - var controller = GenerateSelectableAnimatorContoller(animationTrigger, target); - if (controller != null) - { - if (animator == null) - animator = (target as UXSelectable).gameObject.AddComponent(); + CheckAndSetColorDefaults(colorBlock, targetGraphic); + Rect colorRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(colorBlock)); - UnityEditor.Animations.AnimatorController.SetAnimatorController(animator, controller); - } - } + if (EditorGUILayout.BeginFadeGroup(m_ChildShowColorTint.faded)) + { + EditorGUI.PropertyField(colorRect, colorBlock); } + EditorGUILayout.EndFadeGroup(); + break; + + case Selectable.Transition.SpriteSwap: + CheckAndSetColorDefaults(colorBlock, targetGraphic); + Rect spriteRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(spriteState)); + + if (EditorGUILayout.BeginFadeGroup(m_ChildShowSpriteTrasition.faded)) + { + EditorGUI.PropertyField(spriteRect, spriteState); + } + + EditorGUILayout.EndFadeGroup(); break; } @@ -415,8 +349,51 @@ namespace UnityEngine.UI graphic.canvasRenderer.SetColor(Color.white); } - EditorGUILayout.Space(); - GUILayout.EndVertical(); + EditorGUI.indentLevel--; + } + + private float CalculateTransitionDataHeight(SerializedProperty transitionData) + { + float height = 0; + SerializedProperty transition = transitionData.FindPropertyRelative("transition"); + var currentTransition = GetTransition(transition); + + bool isChild = m_ChildTransitions != null && + !string.IsNullOrEmpty(m_ChildTransitions.propertyPath) && + transitionData.propertyPath.StartsWith(m_ChildTransitions.propertyPath); + + if (isChild && currentTransition == Selectable.Transition.Animation) + { + currentTransition = Selectable.Transition.ColorTint; + } + + height += EditorGUIUtility.singleLineHeight * 1.5f; + height += EditorGUIUtility.singleLineHeight; + + SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic"); + var graphic = targetGraphic.objectReferenceValue as Graphic; + + switch (currentTransition) + { + case Selectable.Transition.ColorTint: + if (graphic == null) height += EditorGUIUtility.singleLineHeight; + break; + case Selectable.Transition.SpriteSwap: + if (!(graphic is Image)) height += EditorGUIUtility.singleLineHeight; + break; + } + + switch (currentTransition) + { + case Selectable.Transition.ColorTint: + height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("colors")); + break; + case Selectable.Transition.SpriteSwap: + height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("spriteState")); + break; + } + + return height; } protected void CheckAndSetColorDefaults(SerializedProperty colorBlock, SerializedProperty targetGraphic) @@ -471,28 +448,34 @@ namespace UnityEngine.UI } } - protected static UnityEditor.Animations.AnimatorController GenerateSelectableAnimatorContoller(SerializedProperty property, UnityEngine.Object targetObj) + #endregion + + #region Static Method + + static Selectable.Transition GetTransition(SerializedProperty transition) { - if (targetObj == null || property == null) + return (Selectable.Transition)transition.enumValueIndex; + } + + private static AnimatorController GenerateSelectableAnimatorContoller(AnimationTriggers animationTriggers, Selectable target) + { + if (target == null) return null; - var targetAsGO = (targetObj as UXSelectable); - var path = GetSaveControllerPath(targetAsGO); + // Where should we create the controller? + var path = GetSaveControllerPath(target); if (string.IsNullOrEmpty(path)) return null; - SerializedProperty normalTrigger = property.FindPropertyRelative("m_NormalTrigger"); - SerializedProperty highlightedTrigger = property.FindPropertyRelative("m_HighlightedTrigger"); - SerializedProperty pressedTrigger = property.FindPropertyRelative("m_PressedTrigger"); - SerializedProperty selectedTrigger = property.FindPropertyRelative("m_SelectedTrigger"); - SerializedProperty disabledTrigger = property.FindPropertyRelative("m_DisabledTrigger"); - var normalName = string.IsNullOrEmpty(normalTrigger.stringValue) ? "Normal" : normalTrigger.stringValue; - var highlightedName = string.IsNullOrEmpty(highlightedTrigger.stringValue) ? "Highlighted" : highlightedTrigger.stringValue; - var pressedName = string.IsNullOrEmpty(pressedTrigger.stringValue) ? "Pressed" : pressedTrigger.stringValue; - var selectedName = string.IsNullOrEmpty(selectedTrigger.stringValue) ? "Selected" : selectedTrigger.stringValue; - var disabledName = string.IsNullOrEmpty(disabledTrigger.stringValue) ? "Disabled" : disabledTrigger.stringValue; + // figure out clip names + var normalName = string.IsNullOrEmpty(animationTriggers.normalTrigger) ? "Normal" : animationTriggers.normalTrigger; + var highlightedName = string.IsNullOrEmpty(animationTriggers.highlightedTrigger) ? "Highlighted" : animationTriggers.highlightedTrigger; + var pressedName = string.IsNullOrEmpty(animationTriggers.pressedTrigger) ? "Pressed" : animationTriggers.pressedTrigger; + var selectedName = string.IsNullOrEmpty(animationTriggers.selectedTrigger) ? "Selected" : animationTriggers.selectedTrigger; + var disabledName = string.IsNullOrEmpty(animationTriggers.disabledTrigger) ? "Disabled" : animationTriggers.disabledTrigger; - var controller = UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(path); + // Create controller and hook up transitions. + var controller = AnimatorController.CreateAnimatorControllerAtPath(path); GenerateTriggerableTransition(normalName, controller); GenerateTriggerableTransition(highlightedName, controller); GenerateTriggerableTransition(pressedName, controller); @@ -504,10 +487,65 @@ namespace UnityEngine.UI return controller; } - private static AnimationClip GenerateTriggerableTransition(string name, UnityEditor.Animations.AnimatorController controller) + private static string GetSaveControllerPath(Selectable target) + { + var defaultName = target.gameObject.name; + var message = string.Format("Create a new animator for the game object '{0}':", defaultName); + return EditorUtility.SaveFilePanelInProject("New Animation Contoller", defaultName, "controller", message); + } + + private static void SetUpCurves(AnimationClip highlightedClip, AnimationClip pressedClip, string animationPath) + { + string[] channels = { "m_LocalScale.x", "m_LocalScale.y", "m_LocalScale.z" }; + + var highlightedKeys = new[] { new Keyframe(0f, 1f), new Keyframe(0.5f, 1.1f), new Keyframe(1f, 1f) }; + var highlightedCurve = new AnimationCurve(highlightedKeys); + foreach (var channel in channels) + AnimationUtility.SetEditorCurve(highlightedClip, EditorCurveBinding.FloatCurve(animationPath, typeof(Transform), channel), highlightedCurve); + + var pressedKeys = new[] { new Keyframe(0f, 1.15f) }; + var pressedCurve = new AnimationCurve(pressedKeys); + foreach (var channel in channels) + AnimationUtility.SetEditorCurve(pressedClip, EditorCurveBinding.FloatCurve(animationPath, typeof(Transform), channel), pressedCurve); + } + + private static string BuildAnimationPath(Selectable target) + { + // if no target don't hook up any curves. + var highlight = target.targetGraphic; + if (highlight == null) + return string.Empty; + + var startGo = highlight.gameObject; + var toFindGo = target.gameObject; + + var pathComponents = new Stack(); + while (toFindGo != startGo) + { + pathComponents.Push(startGo.name); + + // didn't exist in hierarchy! + if (startGo.transform.parent == null) + return string.Empty; + + startGo = startGo.transform.parent.gameObject; + } + + // calculate path + var animPath = new StringBuilder(); + if (pathComponents.Count > 0) + animPath.Append(pathComponents.Pop()); + + while (pathComponents.Count > 0) + animPath.Append("/").Append(pathComponents.Pop()); + + return animPath.ToString(); + } + + private static AnimationClip GenerateTriggerableTransition(string name, AnimatorController controller) { // Create the clip - var clip = UnityEditor.Animations.AnimatorController.AllocateAnimatorClip(name); + var clip = AnimatorController.AllocateAnimatorClip(name); AssetDatabase.AddObjectToAsset(clip, controller); // Create a state in the animatior controller for this clip @@ -519,91 +557,10 @@ namespace UnityEngine.UI // Add an any state transition var stateMachine = controller.layers[0].stateMachine; var transition = stateMachine.AddAnyStateTransition(state); - transition.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, name); + transition.AddCondition(AnimatorConditionMode.If, 0, name); return clip; } - private static string GetSaveControllerPath(UXSelectable target) - { - var defaultName = target != null ? target.gameObject.name : "NewController"; - var message = string.Format("Create a new animator for the game object '{0}':", defaultName); - return EditorUtility.SaveFilePanelInProject("New Animation Contoller", defaultName, "controller", message); - } - - protected static Selectable.Transition GetTransition(SerializedProperty transition) - { - if (transition == null) return Selectable.Transition.None; - return (Selectable.Transition)transition.enumValueIndex; - } - - #endregion - - #region Tab drawing entry - - public override void OnInspectorGUI() - { - serializedObject.Update(); - DrawTabs(); - serializedObject.ApplyModifiedProperties(); - } - - protected void DrawTabs() - { - if (_tabs == null || _tabs.Count == 0) - { - // fallback: draw base directly - DrawSelectableInspector(); - return; - } - - EditorGUILayout.BeginHorizontal(); - - for (int i = 0; i < _tabs.Count; i++) - { - var tab = _tabs[i]; - bool isActive = (i == _currentTabIndex); - var style = new GUIStyle(EditorStyles.toolbarButton) { fixedHeight = 25, fontSize = 11, fontStyle = isActive ? FontStyle.Bold : FontStyle.Normal }; - - var content = new GUIContent(EditorGUIUtility.IconContent(tab.iconName).image, tab.title); - if (GUILayout.Button(content, style)) - { - _currentTabIndex = i; - } - - if (isActive) - { - Rect rect = GUILayoutUtility.GetLastRect(); - EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 2, rect.width, 2), new Color(0.1f, 0.5f, 0.9f)); - } - } - - EditorGUILayout.EndHorizontal(); - GUILayout.Space(4); - - // draw current tab content (call all callbacks registered for the tab) - var callbacks = _tabs[_currentTabIndex].callbacks; - if (callbacks != null) - { - foreach (var cb in callbacks) - { - try - { - cb?.Invoke(); - } - catch (UnityEngine.ExitGUIException) - { - // Unity 用 ExitGUIException 来中断 GUI 流程 —— 需要重新抛出,让 Unity 处理 - throw; - } - catch (Exception e) - { - // 仅记录真正的异常 - Debug.LogException(e); - } - } - } - } - #endregion } } diff --git a/Editor/UX/Slider.meta b/Editor/UX/Slider.meta deleted file mode 100644 index 3d83e93..0000000 --- a/Editor/UX/Slider.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 708a130e1d5f49f18619c6ad9c3baccb -timeCreated: 1765260889 \ No newline at end of file diff --git a/Editor/UX/Slider/UXSliderEditor.cs b/Editor/UX/Slider/UXSliderEditor.cs deleted file mode 100644 index 757079a..0000000 --- a/Editor/UX/Slider/UXSliderEditor.cs +++ /dev/null @@ -1,158 +0,0 @@ -using UnityEditor; -using UnityEditor.UI; - -namespace UnityEngine.UI -{ - [CustomEditor(typeof(UXSlider), true)] - [CanEditMultipleObjects] - internal class UXSliderEditor : UXSelectableEditor - { - SerializedProperty m_Direction; - SerializedProperty m_FillRect; - SerializedProperty m_HandleRect; - SerializedProperty m_MinValue; - SerializedProperty m_MaxValue; - SerializedProperty m_WholeNumbers; - SerializedProperty m_Value; - SerializedProperty m_OnValueChanged; - - SerializedProperty m_SmoothMovement; - SerializedProperty m_SmoothSpeed; - - protected override void OnEnable() - { - base.OnEnable(); - m_FillRect = serializedObject.FindProperty("m_FillRect"); - m_HandleRect = serializedObject.FindProperty("m_HandleRect"); - m_Direction = serializedObject.FindProperty("m_Direction"); - m_MinValue = serializedObject.FindProperty("m_MinValue"); - m_MaxValue = serializedObject.FindProperty("m_MaxValue"); - m_WholeNumbers = serializedObject.FindProperty("m_WholeNumbers"); - m_Value = serializedObject.FindProperty("m_Value"); - m_OnValueChanged = serializedObject.FindProperty("m_OnValueChanged"); - - m_SmoothMovement = serializedObject.FindProperty("m_SmoothMovement"); - m_SmoothSpeed = serializedObject.FindProperty("m_SmoothSpeed"); - - AppendToTab("Image", DrawImage); - } - - protected override void OnDisable() - { - RemoveCallbackFromTab("Image", DrawImage); - base.OnDisable(); - } - - - private void DrawImage() - { - EditorGUILayout.PropertyField(m_FillRect); - EditorGUILayout.PropertyField(m_HandleRect); - - if (m_FillRect.objectReferenceValue != null || m_HandleRect.objectReferenceValue != null) - { - EditorGUI.BeginChangeCheck(); - EditorGUILayout.PropertyField(m_Direction); - if (EditorGUI.EndChangeCheck()) - { - Undo.RecordObjects(serializedObject.targetObjects, "Change Slider Direction"); - - Slider.Direction direction = (Slider.Direction)m_Direction.enumValueIndex; - foreach (var obj in serializedObject.targetObjects) - { - UXSlider slider = obj as UXSlider; - slider.SetDirection(direction, true); - } - } - - EditorGUI.BeginChangeCheck(); - float newMin = EditorGUILayout.FloatField("Min Value", m_MinValue.floatValue); - if (EditorGUI.EndChangeCheck()) - { - if (m_WholeNumbers.boolValue ? Mathf.Round(newMin) < m_MaxValue.floatValue : newMin < m_MaxValue.floatValue) - { - m_MinValue.floatValue = newMin; - if (m_Value.floatValue < newMin) - m_Value.floatValue = newMin; - } - } - - EditorGUI.BeginChangeCheck(); - float newMax = EditorGUILayout.FloatField("Max Value", m_MaxValue.floatValue); - if (EditorGUI.EndChangeCheck()) - { - if (m_WholeNumbers.boolValue ? Mathf.Round(newMax) > m_MinValue.floatValue : newMax > m_MinValue.floatValue) - { - m_MaxValue.floatValue = newMax; - if (m_Value.floatValue > newMax) - m_Value.floatValue = newMax; - } - } - - EditorGUILayout.PropertyField(m_WholeNumbers); - - bool areMinMaxEqual = (m_MinValue.floatValue == m_MaxValue.floatValue); - - if (areMinMaxEqual) - EditorGUILayout.HelpBox("Min Value and Max Value cannot be equal.", MessageType.Warning); - - if (m_WholeNumbers.boolValue) - m_Value.floatValue = Mathf.Round(m_Value.floatValue); - - EditorGUI.BeginDisabledGroup(areMinMaxEqual); - EditorGUI.BeginChangeCheck(); - EditorGUILayout.Slider(m_Value, m_MinValue.floatValue, m_MaxValue.floatValue); - if (EditorGUI.EndChangeCheck()) - { - serializedObject.ApplyModifiedProperties(); - - foreach (var t in targets) - { - if (t is UXSlider slider) - { - slider.onValueChanged?.Invoke(slider.value); - } - } - } - - EditorGUI.EndDisabledGroup(); - - - EditorGUILayout.Space(); - EditorGUILayout.LabelField("Move Smoothing (Gamepad / Keys)", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(m_SmoothMovement, new GUIContent("Enable Smooth Movement")); - using (new EditorGUI.DisabledScope(m_SmoothMovement.boolValue == false)) - { - EditorGUILayout.PropertyField(m_SmoothSpeed, new GUIContent("Smooth Speed (value/sec)")); - if (m_WholeNumbers.boolValue && m_SmoothMovement.boolValue) - { - EditorGUILayout.HelpBox("Note: wholeNumbers is enabled — smooth movement will be disabled at runtime (values stay integer).", MessageType.Info); - } - } - - bool warning = false; - foreach (var obj in serializedObject.targetObjects) - { - UXSlider slider = obj as UXSlider; - Slider.Direction dir = slider.direction; - if (dir == Slider.Direction.LeftToRight || dir == Slider.Direction.RightToLeft) - warning = (slider.navigation.mode != UXNavigation.Mode.Automatic && (slider.FindSelectableOnLeft() != null || slider.FindSelectableOnRight() != null)); - else - warning = (slider.navigation.mode != UXNavigation.Mode.Automatic && (slider.FindSelectableOnDown() != null || slider.FindSelectableOnUp() != null)); - } - - if (warning) - EditorGUILayout.HelpBox("The selected slider direction conflicts with navigation. Not all navigation options may work.", MessageType.Warning); - - EditorGUILayout.Space(); - EditorGUILayout.PropertyField(m_OnValueChanged); - } - else - { - EditorGUILayout.HelpBox("Specify a RectTransform for the slider fill or the slider handle or both. Each must have a parent RectTransform that it can slide within.", MessageType.Info); - } - - serializedObject.ApplyModifiedProperties(); - } - } -} diff --git a/Editor/UX/Slider/UXSliderEditor.cs.meta b/Editor/UX/Slider/UXSliderEditor.cs.meta deleted file mode 100644 index 5404089..0000000 --- a/Editor/UX/Slider/UXSliderEditor.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: e67f2a4d5c0a4f399f67374dcfd42a1f -timeCreated: 1765260896 \ No newline at end of file diff --git a/Editor/UX/Toggle.meta b/Editor/UX/Toggle.meta new file mode 100644 index 0000000..9169b63 --- /dev/null +++ b/Editor/UX/Toggle.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c51b9957a2b441edbcf2d1f264545312 +timeCreated: 1766136377 \ No newline at end of file diff --git a/Editor/UX/Toggle/UXToggleEditor.cs b/Editor/UX/Toggle/UXToggleEditor.cs new file mode 100644 index 0000000..235b694 --- /dev/null +++ b/Editor/UX/Toggle/UXToggleEditor.cs @@ -0,0 +1,178 @@ +using UnityEditor.DrawUtils; +using UnityEditor.Extensions; +using UnityEditor.SceneManagement; +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; + +namespace UnityEditor.UI +{ + [CustomEditor(typeof(UXToggle), true)] + [CanEditMultipleObjects] + internal class UXToggleEditor : UXSelectableEditor + { + SerializedProperty m_OnValueChangedProperty; + SerializedProperty m_TransitionProperty; + SerializedProperty m_GraphicProperty; + SerializedProperty m_GroupProperty; + SerializedProperty m_IsOnProperty; + +#if INPUTSYSTEM_SUPPORT + private SerializedProperty _hotKeyRefrence; + private SerializedProperty _hotkeyPressType; +#endif + + private SerializedProperty hoverAudioClip; + private SerializedProperty clickAudioClip; + + protected override void OnEnable() + { + base.OnEnable(); + + m_TransitionProperty = serializedObject.FindProperty("toggleTransition"); + m_GraphicProperty = serializedObject.FindProperty("graphic"); + m_GroupProperty = serializedObject.FindProperty("m_Group"); + m_IsOnProperty = serializedObject.FindProperty("m_IsOn"); + m_OnValueChangedProperty = serializedObject.FindProperty("onValueChanged"); + +#if INPUTSYSTEM_SUPPORT + _hotKeyRefrence = serializedObject.FindProperty("_hotkeyAction"); + _hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType"); +#endif + + hoverAudioClip = serializedObject.FindProperty("hoverAudioClip"); + clickAudioClip = serializedObject.FindProperty("clickAudioClip"); + + _tabs.AppendToTab("Image", DrawImageTab); + _tabs.RegisterTab("Sound", "d_AudioSource Icon", DrawSoundTab); + _tabs.RegisterTab("Event", "EventTrigger Icon", DrawEventTab); + } + + protected override void OnDisable() + { + base.OnDisable(); + _tabs.UnregisterTab("Sound"); + _tabs.UnregisterTab("Event"); + } + + private void DrawEventTab() + { + EditorGUILayout.Space(); + + serializedObject.Update(); + 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(); + } + + private void DrawSoundTab() + { + GUILayoutHelper.DrawProperty(hoverAudioClip, customSkin, "Hover Sound", "Play", () => + { + if (hoverAudioClip.objectReferenceValue != null) + { + PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue); + } + }); + GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () => + { + if (clickAudioClip.objectReferenceValue != null) + { + PlayAudio((AudioClip)clickAudioClip.objectReferenceValue); + } + }); + } + + private void PlayAudio(AudioClip clip) + { + if (clip != null) + { + ExtensionHelper.PreviewAudioClip(clip); + } + } + + private void DrawImageTab() + { + EditorGUILayout.Space(); + + serializedObject.Update(); + UXToggle toggle = serializedObject.targetObject as UXToggle; + EditorGUI.BeginChangeCheck(); + GUILayoutHelper.DrawProperty(m_IsOnProperty, customSkin, "Is On"); + if (EditorGUI.EndChangeCheck()) + { + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene); + + UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup; + + bool newIsOn = m_IsOnProperty.boolValue; + bool oldIsOn = toggle.isOn; + + // 编辑器下:如果属于某组且不允许 all-off,且当前正是被选中的项,则禁止通过 Inspector 将其关闭 + if (!Application.isPlaying && group != null && !group.allowSwitchOff && oldIsOn && !newIsOn) + { + Debug.LogWarning($"Cannot turn off toggle '{toggle.name}' because its group '{group.name}' does not allow all toggles to be off.", toggle); + // 恢复 Inspector 中的显示为 true + m_IsOnProperty.boolValue = true; + serializedObject.ApplyModifiedProperties(); + } + else + { + // 使用属性赋值以保证视觉刷新(PlayEffect 会在 setter 被调用) + toggle.isOn = newIsOn; + + if (group != null && group.isActiveAndEnabled && toggle.IsActive()) + { + if (toggle.isOn || (!group.AnyTogglesOn() && !group.allowSwitchOff)) + { + toggle.isOn = true; + group.NotifyToggleOn(toggle); + } + } + } + } + + GUILayoutHelper.DrawProperty(m_TransitionProperty, customSkin, "Transition"); + GUILayoutHelper.DrawProperty(m_GraphicProperty, customSkin, "Graphic"); + EditorGUI.BeginChangeCheck(); + GUILayoutHelper.DrawProperty(m_GroupProperty, customSkin, "UXGroup", (oldValue, newValue) => + { + UXToggle self = target as UXToggle; + if (oldValue != null) + { + oldValue.UnregisterToggle(self); + } + + if (newValue != null) + { + newValue.RegisterToggle(self); + } + }); + + if (EditorGUI.EndChangeCheck()) + { + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene); + + UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup; + + // Use the property setter to ensure consistent registration/unregistration + toggle.group = group; + } + + EditorGUILayout.Space(); + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/UX/Toggle/UXToggleEditor.cs.meta b/Editor/UX/Toggle/UXToggleEditor.cs.meta new file mode 100644 index 0000000..2fadedd --- /dev/null +++ b/Editor/UX/Toggle/UXToggleEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9301ee465f2c46d08b2fade637710625 +timeCreated: 1766136386 \ No newline at end of file diff --git a/Runtime/UXComponent/Button/UXButton.cs b/Runtime/UXComponent/Button/UXButton.cs index c00aff5..6a27d96 100644 --- a/Runtime/UXComponent/Button/UXButton.cs +++ b/Runtime/UXComponent/Button/UXButton.cs @@ -1,583 +1,126 @@ using System; using System.Collections; using System.Collections.Generic; +using AlicizaX.UI; using AlicizaX.UI.Extension; -using UnityEngine; -using UnityEngine.Events; using UnityEngine.EventSystems; -using UnityEngine.UI; +#if INPUTSYSTEM_SUPPORT +using UnityEngine.InputSystem; +#endif -[Serializable] -public enum ButtonModeType +namespace UnityEngine.UI { - Normal, - Toggle -} - -[ExecuteAlways] -[DisallowMultipleComponent] -public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler -{ - #region Serialized Fields - - [SerializeField] private ButtonModeType m_Mode; - [SerializeField] private Button.ButtonClickedEvent m_OnClick = new(); - [SerializeField] private List m_ChildTransitions = new(); - [SerializeField] private UXGroup m_UXGroup; - [SerializeField] private AudioClip hoverAudioClip; - [SerializeField] private AudioClip clickAudioClip; - [SerializeField] private UnityEvent m_OnValueChanged = new(); - - #endregion - - #region Private Fields - - private bool m_IsDown; - private bool m_HasExitedWhileDown; - private bool _mTogSelected; - private Coroutine _resetRoutine; - private Coroutine _deferredDeselectRoutine; - private WaitForSeconds _waitFadeDuration; - - // 静态锁(用于 normal 模式点击后的“保持 Selected”并能转移) - private static UXButton s_LockedButton = null; - - private bool m_IsFocusLocked = false; - - private bool m_IsNavFocused = false; - - // ===== 新增:延迟撤销选择的 pending 标记,用于避免在协程尚未完成时读取过时的锁状态 ===== - private bool _deferredDeselectPending = false; - - #endregion - - #region Properties - - public bool Selected + [AddComponentMenu("UI/UXButton", 30)] + public class UXButton : UXSelectable, IPointerClickHandler, ISubmitHandler, IButton +#if INPUTSYSTEM_SUPPORT + , IHotkeyTrigger +#endif { - get => _mTogSelected; - set +#if INPUTSYSTEM_SUPPORT + + InputActionReference IHotkeyTrigger.HotkeyAction { - if ((m_Mode == ButtonModeType.Normal && value) || m_Mode == ButtonModeType.Toggle) + get => _hotkeyAction; + set => _hotkeyAction = value; + } + + EHotkeyPressType IHotkeyTrigger.HotkeyPressType + { + get => _hotkeyPressType; + set => _hotkeyPressType = value; + } + + void IHotkeyTrigger.HotkeyActionTrigger() + { + if (interactable) { - if (m_Mode == ButtonModeType.Toggle) _mTogSelected = !value; - HandleClick(); + OnSubmit(null); } } - } - internal bool InternalTogSelected - { - get => _mTogSelected; - set + [SerializeField] internal InputActionReference _hotkeyAction; + [SerializeField] internal EHotkeyPressType _hotkeyPressType; + + public InputActionReference HotKeyRefrence { - if (_mTogSelected == value) return; - _mTogSelected = value; - m_OnValueChanged?.Invoke(value); - - // 如果当前控件处于聚焦(由导航/SetSelected 进入),那么视觉上应保持 Selected(无论逻辑是否为 selected)。 - bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject; - bool isFocused = m_IsNavFocused || isEventSystemSelected; - - if (m_Mode == ButtonModeType.Toggle && isFocused) - { - // 即使逻辑值为 false,也保持 Selected 视觉,因为焦点还在该控件上 - SetState(SelectionState.Selected); - } - else - { - // 否则按逻辑恢复 Selected / Normal - SetState(value ? SelectionState.Selected : SelectionState.Normal); - } + get { return _hotkeyAction; } } - } - - public Button.ButtonClickedEvent onClick - { - get => m_OnClick; - set => m_OnClick = value; - } - - public UnityEvent onValueChanged - { - get => m_OnValueChanged; - set => m_OnValueChanged = value; - } - - #endregion - - #region Unity Lifecycle - - protected override void Awake() - { - base.Awake(); - // 使用基类 m_MainTransition 的 fadeDuration - _waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_MainTransition.colors.fadeDuration)); - ApplyVisualState(m_SelectionState, true); - } - - protected override void OnDestroy() - { - if (_resetRoutine != null) - StopCoroutine(_resetRoutine); - if (_deferredDeselectRoutine != null) - StopCoroutine(_deferredDeselectRoutine); - base.OnDestroy(); - } +#endif + [SerializeField] private AudioClip hoverAudioClip; + [SerializeField] private AudioClip clickAudioClip; - public override bool IsInteractable() - { - return base.IsInteractable() && m_Interactable; - } - - #endregion - - #region Static lock helper - - private static void SetLockedButton(UXButton newLocked) - { - if (s_LockedButton == newLocked) - return; - - if (s_LockedButton != null) + protected UXButton() { - var old = s_LockedButton; - s_LockedButton = null; - old.m_IsFocusLocked = false; - - if (old._mTogSelected && old.m_Mode == ButtonModeType.Toggle) - old.SetState(SelectionState.Selected); } - if (newLocked != null) + + public override void OnPointerEnter(PointerEventData eventData) { - s_LockedButton = newLocked; - s_LockedButton.m_IsFocusLocked = true; - s_LockedButton.SetState(SelectionState.Selected); - } - } - - #endregion - - #region Pointer Handlers - - public override void OnPointerDown(PointerEventData eventData) - { - if (!CanProcess()) return; - m_IsDown = true; - m_HasExitedWhileDown = false; - SetState(SelectionState.Pressed); - } - - public override void OnPointerUp(PointerEventData eventData) - { - if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left) - return; - - m_IsDown = false; - - if (m_IsFocusLocked || (_mTogSelected && m_Mode == ButtonModeType.Toggle && navigation.mode != UXNavigation.Mode.None)) - { - SetState(SelectionState.Selected); - return; + base.OnPointerEnter(eventData); + PlayAudio(hoverAudioClip); } - var newState = m_HasExitedWhileDown ? SelectionState.Normal : SelectionState.Highlighted; - SetState(newState); - } - - public override void OnPointerEnter(PointerEventData eventData) - { - if (!CanProcessEnter()) return; - m_HasExitedWhileDown = false; - - // 如果 toggle 模式并且聚焦(导航/或 EventSystem 选中),保持 Selected(不触发 Highlight) - bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject; - bool isFocused = m_IsNavFocused || isEventSystemSelected; - if (m_Mode == ButtonModeType.Toggle && isFocused) + public virtual void OnPointerClick(PointerEventData eventData) { - SetState(SelectionState.Selected); - return; - } - - if (navigation.mode != UXNavigation.Mode.None) - { - // ===== 修改:当有 deferred deselect pending 时,不应认为 focus 仍旧被 lock ===== - bool focusLockedEffective = m_IsFocusLocked && !_deferredDeselectPending; - if ((m_Mode == ButtonModeType.Normal && focusLockedEffective) || - (m_Mode == ButtonModeType.Toggle && _mTogSelected)) - { - SetState(SelectionState.Selected); + if (eventData.button != PointerEventData.InputButton.Left) return; - } + + Press(); + PlayAudio(clickAudioClip); } - if (m_IsDown) return; - SetState(SelectionState.Highlighted); - PlayAudio(hoverAudioClip); - } - - public override void OnPointerExit(PointerEventData eventData) - { - if (!m_Interactable) return; - if (m_IsDown) + public virtual void OnSubmit(BaseEventData eventData) { - m_HasExitedWhileDown = true; - return; - } - - // 聚焦时保持 Selected(不回退到 Normal) - bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject; - bool isFocused = m_IsNavFocused || isEventSystemSelected; - if (m_Mode == ButtonModeType.Toggle && isFocused) - { - SetState(SelectionState.Selected); - return; - } - - if (navigation.mode != UXNavigation.Mode.None) - { - // ===== 修改:同上,不要在 deferred pending 时误用旧的 lock 状态 ===== - bool focusLockedEffective = m_IsFocusLocked && !_deferredDeselectPending; - if ((m_Mode == ButtonModeType.Normal && focusLockedEffective) || - (m_Mode == ButtonModeType.Toggle && _mTogSelected)) - { - SetState(SelectionState.Selected); + Press(); + PlayAudio(clickAudioClip); + // if we get set disabled during the press + // don't run the coroutine. + if (!IsActive() || !IsInteractable()) return; - } + + DoStateTransition(SelectionState.Pressed, false); + StartCoroutine(OnFinishSubmit()); } - SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal); - } - - public void OnPointerClick(PointerEventData eventData) - { - if (eventData.button != PointerEventData.InputButton.Left || !m_Interactable) - return; - - PlayAudio(clickAudioClip); - - if (m_Mode == ButtonModeType.Normal) + private IEnumerator OnFinishSubmit() { - if (navigation.mode != UXNavigation.Mode.None) + var fadeTime = colors.fadeDuration; + var elapsedTime = 0f; + + while (elapsedTime < fadeTime) { - if (EventSystem.current != null) - EventSystem.current.SetSelectedGameObject(gameObject, eventData); - - UISystemProfilerApi.AddMarker("Button.onClick", this); - m_OnClick?.Invoke(); - - if (IsStillSelected()) - SetLockedButton(this); + elapsedTime += Time.unscaledDeltaTime; + yield return null; } - else - { - UISystemProfilerApi.AddMarker("Button.onClick", this); - m_OnClick?.Invoke(); - } - } - else - { - HandleClick(); - if (navigation.mode != UXNavigation.Mode.None) - { - if (EventSystem.current != null) - EventSystem.current.SetSelectedGameObject(gameObject, eventData); - - if (IsStillSelected()) - SetLockedButton(this); - } - } - } - - #endregion - - #region Submit handling (Keyboard Submit) - - public void OnSubmit(BaseEventData eventData) - { - if (_resetRoutine != null) - { - StopCoroutine(_resetRoutine); - _resetRoutine = null; + DoStateTransition(currentSelectionState, false); } - if (Animator) + private void PlayAudio(AudioClip clip) { - foreach (int id in _animTriggerCache.Values) - Animator.ResetTrigger(id); + if (clip && UXComponentExtensionsHelper.AudioHelper != null) + UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip); } - if (EventSystem.current != null) - EventSystem.current.SetSelectedGameObject(gameObject, eventData); + [SerializeField] private Button.ButtonClickedEvent m_OnClick = new Button.ButtonClickedEvent(); - m_IsDown = true; - m_HasExitedWhileDown = false; - ForceSetState(SelectionState.Pressed); - PlayAudio(clickAudioClip); - - if (m_Mode == ButtonModeType.Toggle) + public Button.ButtonClickedEvent onClick { - _resetRoutine = StartCoroutine(SubmitToggleDeferredRoutine()); - return; + get { return m_OnClick; } + set { m_OnClick = value; } } - // ① 先执行回调(可能切走焦点) - HandleClick(); - - // ② 只在“焦点仍在自己”时才锁 - if (navigation.mode != UXNavigation.Mode.None && IsStillSelected()) - SetLockedButton(this); - - if (navigation.mode != UXNavigation.Mode.None && m_Mode == ButtonModeType.Normal) - _resetRoutine = StartCoroutine(SubmitAndLockRoutine()); - else - _resetRoutine = StartCoroutine(ResetAfterSubmit()); - } - - #endregion - - #region Selection / Navigation handling - - public override void OnSelect(BaseEventData eventData) - { - if (m_Navigation.mode == UXNavigation.Mode.None) return; - base.OnSelect(eventData); - m_IsNavFocused = true; - - if (s_LockedButton != this) + private void Press() { - SetLockedButton(this); - return; - } + if (!IsActive() || !IsInteractable()) + return; - // 如果聚焦时(无论逻辑是否选中),Toggle 显示 Selected - if (m_Mode == ButtonModeType.Toggle) - { - SetState(SelectionState.Selected); - return; - } - - if (m_IsDown) - { - SetState(SelectionState.Pressed); - } - else - { - if (m_IsFocusLocked) - SetState(SelectionState.Selected); - else - SetState(SelectionState.Highlighted); - } - } - - private IEnumerator DeferredDeselectCheck() - { - // 标记开始:表示我们在等待 EventSystem 正确更新选择(避免竞态) - _deferredDeselectPending = true; - - yield return null; // 等一帧让 EventSystem 更新 currentSelectedGameObject - - bool selectionIsNull = EventSystem.current == null || EventSystem.current.currentSelectedGameObject == null; - - if (selectionIsNull) - { - m_IsNavFocused = false; - // ===== 更安全的解锁:仅当静态锁指向自己时才清理它,避免影响其他按钮 ===== - if (s_LockedButton == this) - s_LockedButton = null; - m_IsFocusLocked = false; - } - - // 取消 pending 标记并清理协程引用 - _deferredDeselectPending = false; - _deferredDeselectRoutine = null; - } - - - public override void OnDeselect(BaseEventData eventData) - { - base.OnDeselect(eventData); - - m_IsNavFocused = false; - - // 停掉上一次的延迟检查(若有) - if (_deferredDeselectRoutine != null) - { - StopCoroutine(_deferredDeselectRoutine); - _deferredDeselectRoutine = null; - _deferredDeselectPending = false; - } - - // 延迟一帧再判断 EventSystem.current.currentSelectedGameObject,避免读取到旧值 - _deferredDeselectRoutine = StartCoroutine(DeferredDeselectCheck()); - - // 视觉状态先按原逻辑处理(立即更新视觉),协程会确保锁状态在正确时刻被清理 - if (m_Mode == ButtonModeType.Toggle) - { - if (_mTogSelected) - SetState(SelectionState.Selected); - else - SetState(SelectionState.Normal); - - return; - } - - if (m_Mode == ButtonModeType.Normal && _mTogSelected) - { - SetState(SelectionState.Selected); - return; - } - - SetState(SelectionState.Normal); - } - - #endregion - - #region Coroutines for submit/reset - - private IEnumerator SubmitAndLockRoutine() - { - yield return _waitFadeDuration; - - m_IsDown = false; - m_HasExitedWhileDown = false; - - if (Animator) - { - foreach (int id in _animTriggerCache.Values) - Animator.ResetTrigger(id); - } - - if (IsStillSelected()) - { - SetLockedButton(this); - ApplyVisualState(SelectionState.Selected, false); - } - - _resetRoutine = null; - } - - - private IEnumerator SubmitToggleDeferredRoutine() - { - yield return null; - yield return _waitFadeDuration; - - HandleClick(); - - if (Animator) - { - foreach (int id in _animTriggerCache.Values) - Animator.ResetTrigger(id); - } - - m_IsDown = false; - m_HasExitedWhileDown = false; - - bool stillFocused = IsStillSelected(); - if (stillFocused) - SetState(SelectionState.Selected); - else - SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal); - - if (navigation.mode != UXNavigation.Mode.None && stillFocused) - SetLockedButton(this); - - _resetRoutine = null; - } - - - private IEnumerator ResetAfterSubmit() - { - yield return _waitFadeDuration; - SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal); - _resetRoutine = null; - } - - #endregion - - #region Utility - - private bool IsStillSelected() - { - if (EventSystem.current != null) - return EventSystem.current.currentSelectedGameObject == gameObject; - return m_IsNavFocused; - } - - #endregion - - #region Logic - - private bool CanProcess() - { - return m_Interactable; - } - - private bool CanProcessEnter() - { - return m_Interactable; - } - - private void HandleClick() - { - if (m_Mode == ButtonModeType.Normal) - { UISystemProfilerApi.AddMarker("Button.onClick", this); - m_OnClick?.Invoke(); - } - else if (m_UXGroup) - { - m_UXGroup.NotifyButtonClicked(this); - } - else - { - InternalTogSelected = !_mTogSelected; - } - } - - /// - /// 设置状态的入口(使用基类 SetState) - /// - /// - private void SetState(SelectionState state) - { - ForceSetState(state); - ApplyVisualState(state, true); - } - - public override void ApplyVisualState(SelectionState state, bool instant) - { - base.ApplyVisualState(state, instant); - for (int i = 0; i < m_ChildTransitions.Count; i++) - base.ApplyTransition(m_ChildTransitions[i], state, instant); - } - - private void PlayAudio(AudioClip clip) - { - if (clip && UXComponentExtensionsHelper.AudioHelper != null) - UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip); - } - - #endregion - - protected override void OnSetProperty() - { - base.OnSetProperty(); - ApplyVisualState(m_SelectionState, true); - } - - public void Focus() - { - if (gameObject != null && EventSystem.current != null) - { - EventSystem.current.SetSelectedGameObject(gameObject); + m_OnClick.Invoke(); } } } diff --git a/Runtime/UXComponent/Group/UXGroup.cs b/Runtime/UXComponent/Group/UXGroup.cs index 8ef1ca0..78c1d7c 100644 --- a/Runtime/UXComponent/Group/UXGroup.cs +++ b/Runtime/UXComponent/Group/UXGroup.cs @@ -1,143 +1,286 @@ +using System; using System.Collections.Generic; -using UnityEngine; -using UnityEngine.Events; +using System.Linq; using UnityEngine.EventSystems; - +using UnityEngine; namespace UnityEngine.UI { + [AddComponentMenu("UI/UXGroup", 31)] [DisallowMultipleComponent] - [ExecuteAlways] public class UXGroup : UIBehaviour { - [SerializeField] private bool m_AllowSwitchOff; - [SerializeField] private List m_Buttons = new(); - - private UXButton _current; - - public UnityEvent onSelectedChanged = new(); + [SerializeField] private bool m_AllowSwitchOff = false; public bool allowSwitchOff { - get => m_AllowSwitchOff; - set + get { return m_AllowSwitchOff; } + set { m_AllowSwitchOff = value; } + } + + [SerializeField] + private List m_Toggles = new List(); + + // 新增:默认选中的 Toggle(可在 Inspector 设置) + [SerializeField] + private UXToggle m_DefaultToggle; + + public UXToggle defaultToggle + { + get { return m_DefaultToggle; } + set { m_DefaultToggle = value; EnsureValidState(); } + } + + protected UXGroup() + { + } + + protected override void Start() + { + EnsureValidState(); + base.Start(); + } + + protected override void OnEnable() + { + EnsureValidState(); + base.OnEnable(); + } + + private void ValidateToggleIsInGroup(UXToggle toggle) + { + if (toggle == null || !m_Toggles.Contains(toggle)) + throw new ArgumentException(string.Format("UXToggle {0} is not part of ToggleGroup {1}", new object[] { toggle, this })); + } + + public void NotifyToggleOn(UXToggle toggle, bool sendCallback = true) + { + ValidateToggleIsInGroup(toggle); + for (var i = 0; i < m_Toggles.Count; i++) { - m_AllowSwitchOff = value; - ValidateGroup(); + if (m_Toggles[i] == toggle) + continue; + + if (sendCallback) + m_Toggles[i].isOn = false; + else + m_Toggles[i].SetIsOnWithoutNotify(false); } } - protected override void Start() => ValidateGroup(); - - protected override void OnDestroy() + public void UnregisterToggle(UXToggle toggle) { - foreach (var btn in m_Buttons) - if (btn) - btn.InternalTogSelected = false; - m_Buttons.Clear(); - base.OnDestroy(); + if (toggle == null) + return; + + if (m_Toggles.Contains(toggle)) + m_Toggles.Remove(toggle); + + // 如果被移除的正好是默认选项,则清空默认项 + if (m_DefaultToggle == toggle) + { + m_DefaultToggle = null; + } } - public void RegisterButton(UXButton button) + public void RegisterToggle(UXToggle toggle) { - if (!button) return; - if (m_Buttons.Contains(button)) return; - m_Buttons.Add(button); + if (toggle == null) + return; - if (button.InternalTogSelected) + if (!m_Toggles.Contains(toggle)) + m_Toggles.Add(toggle); + + // 当组内已有其他开启项,并且不允许 all-off 时, + // 如果新加入的 toggle 本身为 on,则需要把它关闭以维持单选。 + if (!allowSwitchOff) { - if (_current && _current != button) - _current.InternalTogSelected = false; - _current = button; + // 如果已经有一个 active 的 toggle(且不是刚加入的这个),并且刚加入的 toggle isOn 为 true,则关闭刚加入的 toggle + var firstActive = GetFirstActiveToggle(); + if (firstActive != null && firstActive != toggle && toggle.isOn) + { + // 我们使用不触发回调的方式避免编辑时产生不必要的事件调用 + toggle.SetIsOnWithoutNotify(false); + } + else if (firstActive == null) + { + // 没有任何 active 且不允许 all-off:如果 group 指定了 defaultToggle,优先选中它(但仅当 default 在本组内且可交互) + if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle)) + { + var dt = m_DefaultToggle; + if (dt != null && dt != toggle) + { + // 确保默认项被选中(editor/runtime 都适用) + dt.isOn = true; + NotifyToggleOn(dt); + } + else if (dt == toggle) + { + // 新加入项就是默认项,确保它为 on + toggle.isOn = true; + NotifyToggleOn(toggle); + } + } + } + } + } + + // 新增:判断组里是否包含某 toggle(用于运行时/编辑器避免重复注册) + public bool ContainsToggle(UXToggle toggle) + { + return m_Toggles != null && m_Toggles.Contains(toggle); + } + + // Ensure list consistency: clean nulls, and make sure every toggle in this list actually references this group. + public void EnsureValidState() + { + if (m_Toggles == null) + m_Toggles = new List(); + + // Remove null references first + m_Toggles.RemoveAll(x => x == null); + + // 如果不允许 all-off,优先尝试选中 defaultToggle,否则选中第一个。 + if (!allowSwitchOff && !AnyTogglesOn() && m_Toggles.Count != 0) + { + UXToggle toSelect = null; + if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle)) + { + toSelect = m_DefaultToggle; + } + else + { + toSelect = m_Toggles[0]; + } + + if (toSelect != null) + { + toSelect.isOn = true; + NotifyToggleOn(toSelect); + } } - ValidateGroup(); - } + IEnumerable activeToggles = ActiveToggles(); - public void UnregisterButton(UXButton button) - { - if (!button) return; - m_Buttons.Remove(button); - if (_current == button) - _current = null; - button.InternalTogSelected = false; - } - - internal void NotifyButtonClicked(UXButton button) - { - if (!button) return; - - if (button.InternalTogSelected) + if (activeToggles.Count() > 1) { - if (m_AllowSwitchOff) SetSelected(null); + // 如果 defaultToggle 是开启的,优先保留它 + UXToggle firstActive = GetFirstActiveToggle(); + + foreach (UXToggle toggle in activeToggles) + { + if (toggle == firstActive) + { + continue; + } + + toggle.isOn = false; + } + } + + // Synchronize each toggle's group reference to this group if necessary, + // but avoid causing re-ordering in cases where it's already consistent. + for (int i = 0; i < m_Toggles.Count; i++) + { + var t = m_Toggles[i]; + if (t == null) + continue; + + if (t.group != this) + { + // 使用 setter 会触发必要的注册/注销逻辑 + t.group = this; + } + } + } + + public bool AnyTogglesOn() + { + return m_Toggles.Find(x => x != null && x.isOn) != null; + } + + public IEnumerable ActiveToggles() + { + return m_Toggles.Where(x => x != null && x.isOn); + } + + public UXToggle GetFirstActiveToggle() + { + // 优先返回 defaultToggle(如果它处于 on 且在组内) + if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle) && m_DefaultToggle.isOn) + return m_DefaultToggle; + + IEnumerable activeToggles = ActiveToggles(); + return activeToggles.Count() > 0 ? activeToggles.First() : null; + } + + public void SetAllTogglesOff(bool sendCallback = true) + { + bool oldAllowSwitchOff = m_AllowSwitchOff; + m_AllowSwitchOff = true; + + if (sendCallback) + { + for (var i = 0; i < m_Toggles.Count; i++) + if (m_Toggles[i] != null) + m_Toggles[i].isOn = false; } else { - SetSelected(button); + for (var i = 0; i < m_Toggles.Count; i++) + if (m_Toggles[i] != null) + m_Toggles[i].SetIsOnWithoutNotify(false); } + + m_AllowSwitchOff = oldAllowSwitchOff; } - private void SetSelected(UXButton target) + public void Next() { - if (_current == target) return; - var previous = _current; - _current = null; + SelectAdjacent(true); + } - foreach (var btn in m_Buttons) + public void Preview() + { + SelectAdjacent(false); + } + + private void SelectAdjacent(bool forward) + { + if (m_Toggles == null || m_Toggles.Count == 0) + return; + + UXToggle current = GetFirstActiveToggle(); + int currentIndex = current != null ? m_Toggles.IndexOf(current) : -1; + + int idx = currentIndex; + if (idx == -1 && !forward) + idx = 0; + + for (int step = 0; step < m_Toggles.Count; step++) { - bool select = (btn == target); - if (btn.InternalTogSelected != select) - btn.InternalTogSelected = select; - if (select) _current = btn; + if (forward) + { + idx = (idx + 1) % m_Toggles.Count; + } + else + { + idx = (idx - 1 + m_Toggles.Count) % m_Toggles.Count; + } + + UXToggle t = m_Toggles[idx]; + if (t == null) + continue; + + if (!t.IsActive()) + continue; + + if (!t.IsInteractable()) + continue; + + t.isOn = true; + return; } - - if (_current) _current.Focus(); - if (previous != _current) - onSelectedChanged?.Invoke(_current); - } - - private void ValidateGroup() - { - if (_current != null && _current.InternalTogSelected) return; - - if (!m_AllowSwitchOff && m_Buttons.Count > 0) - SetSelected(m_Buttons[0]); - } - - public bool AnyOtherSelected(UXButton exclude) - { - foreach (var btn in m_Buttons) - if (btn != exclude && btn.InternalTogSelected) - return true; - return false; - } - - public void SelectNext() => SelectRelative(1); - public void SelectPrevious() => SelectRelative(-1); - - private void SelectRelative(int dir) - { - if (m_Buttons.Count == 0) return; - int start = _current ? m_Buttons.IndexOf(_current) : -1; - int next = FindNextSelectable(start, dir); - if (next >= 0) SetSelected(m_Buttons[next]); - } - - private int FindNextSelectable(int startIndex, int dir) - { - if (m_Buttons.Count == 0) return -1; - int count = m_Buttons.Count; - int index = (startIndex + dir + count) % count; - - for (int i = 0; i < count; i++) - { - var btn = m_Buttons[index]; - if (btn && btn.isActiveAndEnabled && btn.Interactable) - return index; - index = (index + dir + count) % count; - } - - return -1; } } } diff --git a/Runtime/UXComponent/Group/UXToggle.cs b/Runtime/UXComponent/Group/UXToggle.cs new file mode 100644 index 0000000..3bc60fe --- /dev/null +++ b/Runtime/UXComponent/Group/UXToggle.cs @@ -0,0 +1,260 @@ +using System; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.EventSystems; +#if INPUTSYSTEM_SUPPORT +using UnityEngine.InputSystem; +#endif +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace UnityEngine.UI +{ + [AddComponentMenu("UI/UXToggle", 30)] + [RequireComponent(typeof(RectTransform))] + public class UXToggle : UXSelectable, IPointerClickHandler, ISubmitHandler, ICanvasElement +#if INPUTSYSTEM_SUPPORT + , IHotkeyTrigger +#endif + { + [Serializable] + public class ToggleEvent : UnityEvent { } + + public Toggle.ToggleTransition toggleTransition = Toggle.ToggleTransition.Fade; + public Graphic graphic; + + [SerializeField] private UXGroup m_Group; + public UXGroup group + { + get { return m_Group; } + set + { + SetToggleGroup(value, true); + PlayEffect(true); + } + } + + public ToggleEvent onValueChanged = new ToggleEvent(); + + [Tooltip("Is the toggle currently on or off?")] + [SerializeField] private bool m_IsOn; + + protected UXToggle() { } + +#if UNITY_EDITOR + protected override void OnValidate() + { + base.OnValidate(); + + if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying) + CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this); + } +#endif + + public virtual void Rebuild(CanvasUpdate executing) + { +#if UNITY_EDITOR + if (executing == CanvasUpdate.Prelayout) + onValueChanged.Invoke(m_IsOn); +#endif + } + + public virtual void LayoutComplete() { } + public virtual void GraphicUpdateComplete() { } + + protected override void OnDestroy() + { + if (m_Group != null) + m_Group.EnsureValidState(); + base.OnDestroy(); + } + + protected override void OnEnable() + { + base.OnEnable(); + PlayEffect(true); + } + + protected override void OnDidApplyAnimationProperties() + { + if (graphic != null) + { + bool oldValue = !Mathf.Approximately(graphic.canvasRenderer.GetColor().a, 0); + if (m_IsOn != oldValue) + { + m_IsOn = oldValue; + Set(!oldValue); + } + } + + base.OnDidApplyAnimationProperties(); + } + + // Centralized group setter logic. + private void SetToggleGroup(UXGroup newGroup, bool setMemberValue) + { + // 如果组没有改变,仍然需要确保组里包含此 toggle(修复编辑器中批量拖拽只注册最后一项的问题) + if (m_Group == newGroup) + { + if (setMemberValue) + m_Group = newGroup; + + if (newGroup != null && !newGroup.ContainsToggle(this)) + { + newGroup.RegisterToggle(this); + } + + // 尝试同步组状态,确保编辑器批量赋值时能稳定显示 + if (newGroup != null) + newGroup.EnsureValidState(); + + return; + } + + // 从旧组注销(如果存在) + if (m_Group != null) + m_Group.UnregisterToggle(this); + + if (setMemberValue) + m_Group = newGroup; + + // 注册到新组(不再强依赖 IsActive(),以保证编辑器批量赋值时也能正确注册) + if (newGroup != null) + { + if (!newGroup.ContainsToggle(this)) + { + newGroup.RegisterToggle(this); + } + + // 如果正在 on,通知组(维持单选逻辑) + if (isOn) + newGroup.NotifyToggleOn(this); + + // 同步组的内部状态,确保 Inspector 列表正确显示 + newGroup.EnsureValidState(); + } + } + + public bool isOn + { + get { return m_IsOn; } + set { Set(value); } + } + + public void SetIsOnWithoutNotify(bool value) + { + Set(value, false); + } + + void Set(bool value, bool sendCallback = true) + { + if (m_IsOn == value) + return; + + m_IsOn = value; + if (m_Group != null && m_Group.isActiveAndEnabled && IsActive()) + { + if (m_IsOn || (!m_Group.AnyTogglesOn() && !m_Group.allowSwitchOff)) + { + m_IsOn = true; + m_Group.NotifyToggleOn(this, sendCallback); + } + } + + PlayEffect(toggleTransition == Toggle.ToggleTransition.None); + if (sendCallback) + { + UISystemProfilerApi.AddMarker("Toggle.value", this); + onValueChanged.Invoke(m_IsOn); + } + } + + private void PlayEffect(bool instant) + { + if (graphic == null) + return; + +#if UNITY_EDITOR + if (!Application.isPlaying) + graphic.canvasRenderer.SetAlpha(m_IsOn ? 1f : 0f); + else +#endif + graphic.CrossFadeAlpha(m_IsOn ? 1f : 0f, instant ? 0f : 0.1f, true); + } + + protected override void Start() + { + PlayEffect(true); + } + + private void InternalToggle() + { + if (!IsActive() || !IsInteractable()) + return; + + isOn = !isOn; + } + + public override void OnPointerEnter(PointerEventData eventData) + { + base.OnPointerEnter(eventData); + PlayAudio(hoverAudioClip); + } + + public virtual void OnPointerClick(PointerEventData eventData) + { + if (eventData.button != PointerEventData.InputButton.Left) + return; + + InternalToggle(); + PlayAudio(clickAudioClip); + } + + public virtual void OnSubmit(BaseEventData eventData) + { + InternalToggle(); + PlayAudio(clickAudioClip); + } + + private void PlayAudio(AudioClip clip) + { + if (clip && UXComponentExtensionsHelper.AudioHelper != null) + 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 clickAudioClip; + } +} diff --git a/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs.meta b/Runtime/UXComponent/Group/UXToggle.cs.meta similarity index 64% rename from Runtime/UXComponent/Hotkey/UXHotkeyButton.cs.meta rename to Runtime/UXComponent/Group/UXToggle.cs.meta index 26a2ba6..9e77ff5 100644 --- a/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs.meta +++ b/Runtime/UXComponent/Group/UXToggle.cs.meta @@ -1,11 +1,11 @@ fileFormatVersion: 2 -guid: 42ae64be990942c899bebfffed4b48a5 +guid: 938f629f24004544a889b461e6b7560b MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {fileID: 2800000, guid: 337b039db051cab44819dc51e6af1f43, type: 3} + icon: {fileID: 2800000, guid: ead857c5c78826747a7ab33355cd8108, type: 3} userData: assetBundleName: assetBundleVariant: diff --git a/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs b/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs index 1731c96..97e6c96 100644 --- a/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs +++ b/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs @@ -1,10 +1,10 @@ #if INPUTSYSTEM_SUPPORT +using System.Linq; using AlicizaX.UI.Runtime; using UnityEngine; -namespace AlicizaX.UI +namespace UnityEngine.UI { - // [DisallowMultipleComponent] public class HotkeyBindComponent : MonoBehaviour { private UIHolderObjectBase _holderObjectBase; @@ -18,17 +18,25 @@ namespace AlicizaX.UI private void OnDestroy() { - _holderObjectBase.OnWindowBeforeShowEvent -= BindHotKeys; - _holderObjectBase.OnWindowBeforeClosedEvent -= UnBindHotKeys; + if (_holderObjectBase != null) + { + _holderObjectBase.OnWindowBeforeShowEvent -= BindHotKeys; + _holderObjectBase.OnWindowBeforeClosedEvent -= UnBindHotKeys; + } } - [SerializeField] private UXHotkeyButton[] hotButtons; + // 改成 Component[](或 MonoBehaviour[]),Unity 可以序列化 Component 引用 + [SerializeField] private Component[] hotButtons; internal void BindHotKeys() { + if (hotButtons == null) return; for (int i = 0; i < hotButtons.Length; i++) { - hotButtons[i].BindHotKey(); + if (hotButtons[i] is IHotkeyTrigger trigger) + { + trigger.BindHotKey(); + } } } @@ -36,18 +44,41 @@ namespace AlicizaX.UI [ContextMenu("Bind HotKeys")] private void CollectUXHotkeys() { - hotButtons = gameObject.GetComponentsInChildren(true); + // 更稳健的查找:先拿所有 MonoBehaviour,再 OfType 接口 + var found = gameObject + .GetComponentsInChildren(true) + .OfType() + .Where(t => t.HotkeyAction != null) // 保留原来的筛选条件(如果 HotkeyAction 存在) + .Select(t => t as Component) + .ToArray(); + + hotButtons = found; + + Debug.Log($"[HotkeyBindComponent] 已找到 {hotButtons.Length} 个 UXHotkey 组件并绑定。"); + } + + private void OnValidate() + { + if (_holderObjectBase == null) + { + _holderObjectBase = gameObject.GetComponent(); + } + // 在编辑器模式下自动收集(可根据需求移除) + CollectUXHotkeys(); } #endif internal void UnBindHotKeys() { + if (hotButtons == null) return; for (int i = 0; i < hotButtons.Length; i++) { - hotButtons[i].UnBindHotKey(); + if (hotButtons[i] is IHotkeyTrigger trigger) + { + trigger.UnBindHotKey(); + } } } } } - #endif diff --git a/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs b/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs new file mode 100644 index 0000000..224831f --- /dev/null +++ b/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs @@ -0,0 +1,14 @@ +#if INPUTSYSTEM_SUPPORT +using UnityEngine.InputSystem; + +namespace UnityEngine.UI +{ + public interface IHotkeyTrigger + { + internal InputActionReference HotkeyAction { get; set; } + internal EHotkeyPressType HotkeyPressType { get; set; } + internal void HotkeyActionTrigger(); + } +} + +#endif diff --git a/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs.meta b/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs.meta new file mode 100644 index 0000000..0e45ea7 --- /dev/null +++ b/Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1b8deefaf6dc42559bed29ea82e80164 +timeCreated: 1766142335 \ No newline at end of file diff --git a/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs b/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs deleted file mode 100644 index 2deaed2..0000000 --- a/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if INPUTSYSTEM_SUPPORT -using UnityEngine; -using UnityEngine.InputSystem; - -namespace AlicizaX.UI -{ - [DisallowMultipleComponent] - public class UXHotkeyButton : UXButton - { - [SerializeField] internal InputActionReference _hotKeyRefrence; - [SerializeField] internal EHotkeyPressType _hotkeyPressType; - - public InputActionReference HotKeyRefrence - { - get { return _hotKeyRefrence; } - } - - internal void HotkeyActionTrigger() - { - if (Interactable) - { - OnSubmit(null); - } - } - } -} - -#endif diff --git a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs index 00a18eb..18cdf58 100644 --- a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs +++ b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs @@ -3,10 +3,9 @@ using System; using UnityEngine; using UnityEngine.InputSystem; using System.Collections.Generic; -using System.Collections; -using AlicizaX.UI; +using UnityEngine.UI; -namespace AlicizaX.UI +namespace UnityEngine.UI { internal enum EHotkeyPressType { @@ -19,9 +18,9 @@ namespace AlicizaX.UI private readonly struct HotkeyRegistration { public readonly EHotkeyPressType pressType; - public readonly UXHotkeyButton button; + public readonly IHotkeyTrigger button; - public HotkeyRegistration(UXHotkeyButton btn, EHotkeyPressType pressType) + public HotkeyRegistration(IHotkeyTrigger btn, EHotkeyPressType pressType) { button = btn; this.pressType = pressType; @@ -35,7 +34,7 @@ namespace AlicizaX.UI new(32); - private static readonly Dictionary _buttonRegistrations = new(64); + private static readonly Dictionary _buttonRegistrations = new(64); #if UNITY_EDITOR @@ -53,7 +52,7 @@ namespace AlicizaX.UI } #endif - internal static void RegisterHotkey(UXHotkeyButton button, InputActionReference action, EHotkeyPressType pressType) + internal static void RegisterHotkey(IHotkeyTrigger button, InputActionReference action, EHotkeyPressType pressType) { if (action == null || action.action == null || button == null) return; @@ -94,7 +93,7 @@ namespace AlicizaX.UI } } - public static void UnregisterHotkey(UXHotkeyButton button) + public static void UnregisterHotkey(IHotkeyTrigger button) { if (button == null || !_buttonRegistrations.TryGetValue(button, out var actionId)) return; @@ -153,24 +152,24 @@ namespace AlicizaX.UI public static class UXHotkeyHotkeyExtension { - public static void BindHotKey(this UXHotkeyButton button) + public static void BindHotKey(this IHotkeyTrigger button) { if (button == null) return; - if (button._hotKeyRefrence != null) + if (button.HotkeyAction != null) { UXHotkeyRegisterManager.RegisterHotkey( button, - button._hotKeyRefrence, - button._hotkeyPressType + button.HotkeyAction, + button.HotkeyPressType ); } } - public static void UnBindHotKey(this UXHotkeyButton button) + public static void UnBindHotKey(this IHotkeyTrigger button) { - if (button == null || button._hotKeyRefrence == null) return; + if (button == null || button.HotkeyAction == null) return; UXHotkeyRegisterManager.UnregisterHotkey(button); } } diff --git a/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs b/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs deleted file mode 100644 index ff79ad8..0000000 --- a/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs +++ /dev/null @@ -1,133 +0,0 @@ -using UnityEngine.EventSystems; - -namespace UnityEngine.UI -{ - internal static class MultipleDisplayUtilities - { - /// - /// Converts the current drag position into a relative position for the display. - /// - /// - /// - /// Returns true except when the drag operation is not on the same display as it originated - public static bool GetRelativeMousePositionForDrag(PointerEventData eventData, ref Vector2 position) - { - #if UNITY_EDITOR - position = eventData.position; - #else - int pressDisplayIndex = eventData.pointerPressRaycast.displayIndex; - var relativePosition = RelativeMouseAtScaled(eventData.position); - int currentDisplayIndex = (int)relativePosition.z; - - // Discard events on a different display. - if (currentDisplayIndex != pressDisplayIndex) - return false; - - // If we are not on the main display then we must use the relative position. - position = pressDisplayIndex != 0 ? (Vector2)relativePosition : eventData.position; - #endif - return true; - } - - internal static Vector3 GetRelativeMousePositionForRaycast(PointerEventData eventData) - { - // The multiple display system is not supported on all platforms, when it is not supported the returned position - // will be all zeros so when the returned index is 0 we will default to the event data to be safe. - Vector3 eventPosition = RelativeMouseAtScaled(eventData.position); - if (eventPosition == Vector3.zero) - { - eventPosition = eventData.position; -#if UNITY_EDITOR - eventPosition.z = Display.activeEditorGameViewTarget; -#endif - // We don't really know in which display the event occurred. We will process the event assuming it occurred in our display. - } - - // We support multiple display on some platforms. When supported: - // - InputSystem will set eventData.displayIndex - // - Old Input System will set eventPosition.z - // - // If the event is on the main display, both displayIndex and eventPosition.z - // will be 0 so in that case we can leave the eventPosition untouched (see UUM-47650). -#if ENABLE_INPUT_SYSTEM && PACKAGE_INPUTSYSTEM - if (eventData.displayIndex > 0) - eventPosition.z = eventData.displayIndex; -#endif - - return eventPosition; - } - - /// - /// A version of Display.RelativeMouseAt that scales the position when the main display has a different rendering resolution to the system resolution. - /// By default, the mouse position is relative to the main render area, we need to adjust this so it is relative to the system resolution - /// in order to correctly determine the position on other displays. - /// - /// - public static Vector3 RelativeMouseAtScaled(Vector2 position) - { - #if !UNITY_EDITOR && !UNITY_WSA - // If the main display is now the same resolution as the system then we need to scale the mouse position. (case 1141732) - if (Display.main.renderingWidth != Display.main.systemWidth || Display.main.renderingHeight != Display.main.systemHeight) - { - // The system will add padding when in full-screen and using a non-native aspect ratio. (case UUM-7893) - // For example Rendering 1920x1080 with a systeem resolution of 3440x1440 would create black bars on each side that are 330 pixels wide. - // we need to account for this or it will offset our coordinates when we are not on the main display. - var systemAspectRatio = Display.main.systemWidth / (float)Display.main.systemHeight; - - var sizePlusPadding = new Vector2(Display.main.renderingWidth, Display.main.renderingHeight); - var padding = Vector2.zero; - if (Screen.fullScreen) - { - var aspectRatio = Screen.width / (float)Screen.height; - if (Display.main.systemHeight * aspectRatio < Display.main.systemWidth) - { - // Horizontal padding - sizePlusPadding.x = Display.main.renderingHeight * systemAspectRatio; - padding.x = (sizePlusPadding.x - Display.main.renderingWidth) * 0.5f; - } - else - { - // Vertical padding - sizePlusPadding.y = Display.main.renderingWidth / systemAspectRatio; - padding.y = (sizePlusPadding.y - Display.main.renderingHeight) * 0.5f; - } - } - - var sizePlusPositivePadding = sizePlusPadding - padding; - - // If we are not inside of the main display then we must adjust the mouse position so it is scaled by - // the main display and adjusted for any padding that may have been added due to different aspect ratios. - if (position.y < -padding.y || position.y > sizePlusPositivePadding.y || - position.x < -padding.x || position.x > sizePlusPositivePadding.x) - { - var adjustedPosition = position; - - if (!Screen.fullScreen) - { - // When in windowed mode, the window will be centered with the 0,0 coordinate at the top left, we need to adjust so it is relative to the screen instead. - adjustedPosition.x -= (Display.main.renderingWidth - Display.main.systemWidth) * 0.5f; - adjustedPosition.y -= (Display.main.renderingHeight - Display.main.systemHeight) * 0.5f; - } - else - { - // Scale the mouse position to account for the black bars when in a non-native aspect ratio. - adjustedPosition += padding; - adjustedPosition.x *= Display.main.systemWidth / sizePlusPadding.x; - adjustedPosition.y *= Display.main.systemHeight / sizePlusPadding.y; - } - - var relativePos = Display.RelativeMouseAt(adjustedPosition); - - // If we are not on the main display then return the adjusted position. - if (relativePos.z != 0) - return relativePos; - } - - // We are using the main display. - return new Vector3(position.x, position.y, 0); - } - #endif - return Display.RelativeMouseAt(position); - } - } -} diff --git a/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs.meta b/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs.meta deleted file mode 100644 index 83495e7..0000000 --- a/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: f5fbd4c3b1c84b51abda86322e214b05 -timeCreated: 1765260757 \ No newline at end of file diff --git a/Runtime/UXComponent/Selectable/SetPropertyUtility.cs b/Runtime/UXComponent/Selectable/SetPropertyUtility.cs deleted file mode 100644 index 3719979..0000000 --- a/Runtime/UXComponent/Selectable/SetPropertyUtility.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace UnityEngine.UI -{ - internal static class SetPropertyUtility - { - public static bool SetColor(ref Color currentValue, Color newValue) - { - if (currentValue.r == newValue.r && currentValue.g == newValue.g && currentValue.b == newValue.b && currentValue.a == newValue.a) - return false; - - currentValue = newValue; - return true; - } - - public static bool SetStruct(ref T currentValue, T newValue) where T : struct - { - if (EqualityComparer.Default.Equals(currentValue, newValue)) - return false; - - currentValue = newValue; - return true; - } - - public static bool SetClass(ref T currentValue, T newValue) where T : class - { - if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue))) - return false; - - currentValue = newValue; - return true; - } - } -} diff --git a/Runtime/UXComponent/Selectable/SetPropertyUtility.cs.meta b/Runtime/UXComponent/Selectable/SetPropertyUtility.cs.meta deleted file mode 100644 index bb0ed62..0000000 --- a/Runtime/UXComponent/Selectable/SetPropertyUtility.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: d3d40fd183ef410e995334bdca8e4638 -timeCreated: 1764668263 \ No newline at end of file diff --git a/Runtime/UXComponent/Selectable/UXNavigation.cs b/Runtime/UXComponent/Selectable/UXNavigation.cs deleted file mode 100644 index 0f6b962..0000000 --- a/Runtime/UXComponent/Selectable/UXNavigation.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using UnityEngine; -using UnityEngine.UI; - -namespace UnityEngine.UI -{ - [Serializable] - public struct UXNavigation : IEquatable - { - [Flags] - public enum Mode - { - None = 0, - Horizontal = 1, - Vertical = 2, - Automatic = 3, - Explicit = 4, - } - - [SerializeField] private Mode m_Mode; - - [HideInInspector] [SerializeField] private bool m_WrapAround; - - [SerializeField] private UXSelectable m_SelectOnUp; - [SerializeField] private UXSelectable m_SelectOnDown; - [SerializeField] private UXSelectable m_SelectOnLeft; - [SerializeField] private UXSelectable m_SelectOnRight; - - public Mode mode - { - get { return m_Mode; } - set { m_Mode = value; } - } - - public bool wrapAround - { - get { return m_WrapAround; } - set { m_WrapAround = value; } - } - - public UXSelectable selectOnUp - { - get { return m_SelectOnUp; } - set { m_SelectOnUp = value; } - } - - public UXSelectable selectOnDown - { - get { return m_SelectOnDown; } - set { m_SelectOnDown = value; } - } - - public UXSelectable selectOnLeft - { - get { return m_SelectOnLeft; } - set { m_SelectOnLeft = value; } - } - - public UXSelectable selectOnRight - { - get { return m_SelectOnRight; } - set { m_SelectOnRight = value; } - } - - static public UXNavigation defaultNavigation - { - get - { - var defaultNav = new UXNavigation(); - defaultNav.m_Mode = Mode.Automatic; - defaultNav.m_WrapAround = false; - return defaultNav; - } - } - - public bool Equals(UXNavigation other) - { - return mode == other.mode && - selectOnUp == other.selectOnUp && - selectOnDown == other.selectOnDown && - selectOnLeft == other.selectOnLeft && - selectOnRight == other.selectOnRight; - } - } -} diff --git a/Runtime/UXComponent/Selectable/UXNavigation.cs.meta b/Runtime/UXComponent/Selectable/UXNavigation.cs.meta deleted file mode 100644 index d9c0cb2..0000000 --- a/Runtime/UXComponent/Selectable/UXNavigation.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 46a321e196774407b8f17943e7b55c63 -timeCreated: 1764667489 \ No newline at end of file diff --git a/Runtime/UXComponent/Selectable/UXSelectable.cs b/Runtime/UXComponent/Selectable/UXSelectable.cs index a30ad55..f0024c0 100644 --- a/Runtime/UXComponent/Selectable/UXSelectable.cs +++ b/Runtime/UXComponent/Selectable/UXSelectable.cs @@ -11,666 +11,107 @@ namespace UnityEngine.UI public Selectable.Transition transition = Selectable.Transition.ColorTint; public ColorBlock colors = ColorBlock.defaultColorBlock; public SpriteState spriteState; - public AnimationTriggers animationTriggers = new AnimationTriggers(); } - - [ExecuteAlways] - [SelectionBase] - [DisallowMultipleComponent] - public class UXSelectable : UIBehaviour, - IMoveHandler, - IPointerDownHandler, IPointerUpHandler, - IPointerEnterHandler, IPointerExitHandler, - ISelectHandler, IDeselectHandler + public class UXSelectable : Selectable { - [Serializable] - public enum SelectionState + [SerializeField] private List m_ChildTransitions = new(); + + void StartChildColorTween(TransitionData transitionData, Color targetColor, bool instant) { - Normal, - Highlighted, - Pressed, - Selected, - Disabled, + if (transitionData.targetGraphic == null) + return; + transitionData.targetGraphic.CrossFadeColor(targetColor, instant ? 0f : transitionData.colors.fadeDuration, true, true); } - #region Fields - - [SerializeField] protected UXNavigation m_Navigation = UXNavigation.defaultNavigation; - - [SerializeField] protected bool m_Interactable = true; - [SerializeField] protected TransitionData m_MainTransition = new TransitionData(); - - // current visual / logical selection state (now in base) - [SerializeField] protected SelectionState m_SelectionState = SelectionState.Normal; - - // runtime event flags (used by currentSelectionState) - protected bool isPointerInside { get; set; } - protected bool isPointerDown { get; set; } - protected bool hasSelection { get; set; } - - protected bool m_GroupsAllowInteraction = true; - private readonly List m_CanvasGroupCache = new List(); - - // registry for navigation find functions - protected static UXSelectable[] s_Selectables = new UXSelectable[10]; - protected static int s_SelectableCount = 0; - protected int m_CurrentIndex = -1; - protected bool m_EnableCalled = false; - [SerializeField] private Animator _animator; - public Animator Animator => _animator ? _animator : _animator = GetComponent(); - - public static readonly Dictionary _animTriggerCache = new() + void DoChildSpriteSwap(TransitionData transitionData, Sprite newSprite) { - { "Normal", Animator.StringToHash("Normal") }, - { "Highlighted", Animator.StringToHash("Highlighted") }, - { "Pressed", Animator.StringToHash("Pressed") }, - { "Selected", Animator.StringToHash("Selected") }, - { "Disabled", Animator.StringToHash("Disabled") }, - }; - - - public Graphic targetGraphic - { - get { return m_MainTransition.targetGraphic; } - set - { - if (SetPropertyUtility.SetClass(ref m_MainTransition.targetGraphic, value)) OnSetProperty(); - } - } - - #endregion - - #region Properties - - public UXNavigation navigation - { - get { return m_Navigation; } - set - { - if (SetPropertyUtility.SetStruct(ref m_Navigation, value)) - OnSetProperty(); - } - } - - public bool Interactable - { - get => m_Interactable; - set - { - if (m_Interactable == value) return; - m_Interactable = value; - OnSetProperty(); - } - } - - public static UXSelectable[] allSelectablesArray - { - get - { - UXSelectable[] temp = new UXSelectable[s_SelectableCount]; - Array.Copy(s_Selectables, temp, s_SelectableCount); - return temp; - } - } - - #endregion - - #region Unity Lifecycle - - protected override void Awake() - { - base.Awake(); - // nothing specific here; subclasses may initialize visuals - } - - protected override void OnEnable() - { - if (m_EnableCalled) + if (transitionData.targetGraphic == null) return; - base.OnEnable(); - - if (s_SelectableCount == s_Selectables.Length) - { - UXSelectable[] temp = new UXSelectable[s_Selectables.Length * 2]; - Array.Copy(s_Selectables, temp, s_Selectables.Length); - s_Selectables = temp; - } - - if (EventSystem.current && EventSystem.current.currentSelectedGameObject == gameObject) - { - hasSelection = true; - } - - m_CurrentIndex = s_SelectableCount; - s_Selectables[m_CurrentIndex] = this; - s_SelectableCount++; - isPointerDown = false; - m_GroupsAllowInteraction = ParentGroupAllowsInteraction(); - - m_EnableCalled = true; - - // Ensure visual state matches current settings immediately - OnSetProperty(); + if (transitionData.targetGraphic is Image img) + img.overrideSprite = newSprite; } - - protected override void OnDisable() - { - if (!m_EnableCalled) - return; - - s_SelectableCount--; - - s_Selectables[s_SelectableCount].m_CurrentIndex = m_CurrentIndex; - s_Selectables[m_CurrentIndex] = s_Selectables[s_SelectableCount]; - s_Selectables[s_SelectableCount] = null; - - InstantClearState(); - - base.OnDisable(); - m_EnableCalled = false; - } - - protected void OnCanvasGroupChanged() - { - var parentGroupAllows = ParentGroupAllowsInteraction(); - if (parentGroupAllows != m_GroupsAllowInteraction) - { - m_GroupsAllowInteraction = parentGroupAllows; - OnSetProperty(); - } - } - - #endregion - - #region Groups & Interactable - - protected bool ParentGroupAllowsInteraction() - { - Transform t = transform; - while (t != null) - { - t.GetComponents(m_CanvasGroupCache); - for (var i = 0; i < m_CanvasGroupCache.Count; i++) - { - if (m_CanvasGroupCache[i].enabled && !m_CanvasGroupCache[i].interactable) - return false; - if (m_CanvasGroupCache[i].ignoreParentGroups) - return true; - } - - t = t.parent; - } - - return true; - } - - public virtual bool IsInteractable() - { - return m_GroupsAllowInteraction && m_Interactable; - } - - #endregion - - #region Property change handling & SelectionState API - - /// - /// Called when a property that affects visuals changes (navigation/interactable/animation/etc). - /// - protected virtual void OnSetProperty() - { - // If not interactable => Disabled state - if (!IsInteractable()) - { - m_SelectionState = SelectionState.Disabled; - DoStateTransition(SelectionState.Disabled, false); - } - else - { - // If previously disabled, restore to Normal - if (m_SelectionState == SelectionState.Disabled) - m_SelectionState = SelectionState.Normal; - - // Apply current selection state - DoStateTransition(m_SelectionState, false); - } - } - - /// - /// Computed selection state based on pointer / selection flags and interactability. - /// Mirrors Unity's Selectable.currentSelectionState behavior. - /// - protected SelectionState currentSelectionState - { - get - { - if (!IsInteractable()) - return SelectionState.Disabled; - if (isPointerDown) - return SelectionState.Pressed; - if (hasSelection) - return SelectionState.Selected; - if (isPointerInside) - return SelectionState.Highlighted; - return SelectionState.Normal; - } - } - - /// - /// Evaluate and transition to the computed selection state. Call after changing pointer/selection flags. - /// - protected void EvaluateAndTransitionToSelectionState() - { - if (!IsActive() || !IsInteractable()) - return; - - DoStateTransition(currentSelectionState, false); - } - - /// - /// Force apply visual state immediately. - /// - protected void ForceSetState(SelectionState state) - { - m_SelectionState = state; - DoStateTransition(state, true); - } - - /// - /// Clear internal state and restore visuals (used on disable / focus lost). - /// - protected virtual void InstantClearState() - { - // reset flags - isPointerInside = false; - isPointerDown = false; - hasSelection = false; - - // restore visuals based on main transition's normal state - if (m_MainTransition == null) return; - - switch (m_MainTransition.transition) - { - case Selectable.Transition.ColorTint: - TweenColor(m_MainTransition, m_MainTransition.colors.normalColor * m_MainTransition.colors.colorMultiplier, true); - break; - case Selectable.Transition.SpriteSwap: - SwapSprite(m_MainTransition, null); - break; - case Selectable.Transition.Animation: - PlayAnimation(m_MainTransition.animationTriggers.normalTrigger); - break; - } - - m_SelectionState = SelectionState.Normal; - } - - /// - /// Core mapping from SelectionState -> TransitionData and execution. - /// Subclasses can override PlayAnimation or ApplyVisualState for custom behavior. - /// - /// - /// - protected virtual void DoStateTransition(SelectionState state, bool instant) - { - if (!gameObject.activeInHierarchy) - return; - - if (m_MainTransition == null) - return; - - Color tintColor = Color.white; - Sprite sprite = null; - string trigger = null; - - switch (state) - { - case SelectionState.Normal: - tintColor = m_MainTransition.colors.normalColor; - sprite = m_MainTransition.spriteState.highlightedSprite; - trigger = m_MainTransition.animationTriggers.normalTrigger; - break; - case SelectionState.Highlighted: - tintColor = m_MainTransition.colors.highlightedColor; - sprite = m_MainTransition.spriteState.highlightedSprite; - trigger = m_MainTransition.animationTriggers.highlightedTrigger; - break; - case SelectionState.Pressed: - tintColor = m_MainTransition.colors.pressedColor; - sprite = m_MainTransition.spriteState.pressedSprite; - trigger = m_MainTransition.animationTriggers.pressedTrigger; - break; - case SelectionState.Selected: - tintColor = m_MainTransition.colors.selectedColor; - sprite = m_MainTransition.spriteState.selectedSprite; - trigger = m_MainTransition.animationTriggers.selectedTrigger; - break; - case SelectionState.Disabled: - tintColor = m_MainTransition.colors.disabledColor; - sprite = m_MainTransition.spriteState.disabledSprite; - trigger = m_MainTransition.animationTriggers.disabledTrigger; - break; - } - - // Execute appropriate transition type - switch (m_MainTransition.transition) - { - case Selectable.Transition.ColorTint: - TweenColor(m_MainTransition, tintColor * m_MainTransition.colors.colorMultiplier, instant); - break; - case Selectable.Transition.SpriteSwap: - SwapSprite(m_MainTransition, sprite); - break; - case Selectable.Transition.Animation: - PlayAnimation(trigger); - break; - case Selectable.Transition.None: - default: - break; - } - - // keep serialized state in sync - m_SelectionState = state; - } - - #endregion - - #region Navigation helpers - - public UXSelectable FindSelectable(Vector3 dir) - { - dir = dir.normalized; - Vector3 localDir = Quaternion.Inverse(transform.rotation) * dir; - Vector3 pos = transform.TransformPoint(GetPointOnRectEdge(transform as RectTransform, localDir)); - float maxScore = Mathf.NegativeInfinity; - float maxFurthestScore = Mathf.NegativeInfinity; - float score = 0; - - bool wantsWrapAround = navigation.wrapAround && (m_Navigation.mode == UXNavigation.Mode.Vertical || m_Navigation.mode == UXNavigation.Mode.Horizontal); - - UXSelectable bestPick = null; - UXSelectable bestFurthestPick = null; - - for (int i = 0; i < s_SelectableCount; ++i) - { - UXSelectable sel = s_Selectables[i]; - - if (sel == this) - continue; - - if (!sel.IsInteractable() || sel.navigation.mode == UXNavigation.Mode.None) - continue; - #if UNITY_EDITOR - if (Camera.current != null && !UnityEditor.SceneManagement.StageUtility.IsGameObjectRenderedByCamera(sel.gameObject, Camera.current)) - continue; + protected override void OnValidate() + { + if (isActiveAndEnabled) + { + for (int i = 0; i < m_ChildTransitions.Count; i++) + { + DoChildSpriteSwap(m_ChildTransitions[i], null); + StartChildColorTween(m_ChildTransitions[i], Color.white, true); + } + } + + base.OnValidate(); + } #endif - var selRect = sel.transform as RectTransform; - Vector3 selCenter = selRect != null ? (Vector3)selRect.rect.center : Vector3.zero; - Vector3 myVector = sel.transform.TransformPoint(selCenter) - pos; - - float dot = Vector3.Dot(dir, myVector); - - if (wantsWrapAround && dot < 0) + protected override void InstantClearState() + { + base.InstantClearState(); + for (int i = 0; i < m_ChildTransitions.Count; i++) + { + switch (transition) { - score = -dot * myVector.sqrMagnitude; - if (score > maxFurthestScore) - { - maxFurthestScore = score; - bestFurthestPick = sel; - } - - continue; - } - - if (dot <= 0) - continue; - - score = dot / myVector.sqrMagnitude; - - if (score > maxScore) - { - maxScore = score; - bestPick = sel; + case Transition.ColorTint: + StartChildColorTween(m_ChildTransitions[i], Color.white, true); + break; + case Transition.SpriteSwap: + DoChildSpriteSwap(m_ChildTransitions[i], null); + break; } } - - if (wantsWrapAround && null == bestPick) return bestFurthestPick; - - return bestPick; } - private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir) + protected override void DoStateTransition(SelectionState state, bool instant) { - if (rect == null) return Vector3.zero; - if (dir != Vector2.zero) dir /= Mathf.Max(Mathf.Abs(dir.x), Mathf.Abs(dir.y)); - dir = rect.rect.center + Vector2.Scale(rect.rect.size, dir * 0.5f); - return dir; - } + base.DoStateTransition(state, instant); - public virtual UXSelectable FindSelectableOnLeft() - { - if (m_Navigation.mode == UXNavigation.Mode.Explicit) - if (m_Navigation.selectOnLeft != null && m_Navigation.selectOnLeft.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None) - return m_Navigation.selectOnLeft; - - if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0) - return FindSelectable(transform.rotation * Vector3.left); - return null; - } - - public virtual UXSelectable FindSelectableOnRight() - { - if (m_Navigation.mode == UXNavigation.Mode.Explicit) - if (m_Navigation.selectOnRight != null && m_Navigation.selectOnRight.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None) - return m_Navigation.selectOnRight; - if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0) - return FindSelectable(transform.rotation * Vector3.right); - return null; - } - - public virtual UXSelectable FindSelectableOnUp() - { - if (m_Navigation.mode == UXNavigation.Mode.Explicit) - if (m_Navigation.selectOnUp != null && m_Navigation.selectOnUp.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None) - return m_Navigation.selectOnUp; - if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0) - return FindSelectable(transform.rotation * Vector3.up); - return null; - } - - public virtual UXSelectable FindSelectableOnDown() - { - if (m_Navigation.mode == UXNavigation.Mode.Explicit) - if (m_Navigation.selectOnDown != null && m_Navigation.selectOnDown.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None) - return m_Navigation.selectOnDown; - if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0) - return FindSelectable(transform.rotation * Vector3.down); - return null; - } - - public virtual void OnMove(AxisEventData eventData) - { - switch (eventData.moveDir) + for (int i = 0; i < m_ChildTransitions.Count; i++) { - case MoveDirection.Right: - Navigate(eventData, FindSelectableOnRight()); - break; - case MoveDirection.Up: - Navigate(eventData, FindSelectableOnUp()); - break; - case MoveDirection.Left: - Navigate(eventData, FindSelectableOnLeft()); - break; - case MoveDirection.Down: - Navigate(eventData, FindSelectableOnDown()); - break; + TransitionData transitionData = m_ChildTransitions[i]; + Color tintColor; + Sprite transitionSprite; + switch (state) + { + case SelectionState.Normal: + tintColor = transitionData.colors.normalColor; + transitionSprite = null; + break; + case SelectionState.Highlighted: + tintColor = transitionData.colors.highlightedColor; + transitionSprite = transitionData.spriteState.highlightedSprite; + break; + case SelectionState.Pressed: + tintColor = transitionData.colors.pressedColor; + transitionSprite = transitionData.spriteState.pressedSprite; + break; + case SelectionState.Selected: + tintColor = transitionData.colors.selectedColor; + transitionSprite = transitionData.spriteState.selectedSprite; + break; + case SelectionState.Disabled: + tintColor = transitionData.colors.disabledColor; + transitionSprite = transitionData.spriteState.disabledSprite; + break; + default: + tintColor = Color.black; + transitionSprite = null; + break; + } + + switch (transition) + { + case Transition.ColorTint: + StartChildColorTween(transitionData, tintColor * transitionData.colors.colorMultiplier, instant); + break; + case Transition.SpriteSwap: + DoChildSpriteSwap(transitionData, transitionSprite); + break; + } } } - - void Navigate(AxisEventData eventData, UXSelectable sel) - { - if (sel != null && sel.IsActive()) - eventData.selectedObject = sel.gameObject; - } - - #endregion - - #region Pointer / Select base implementations (update flags + evaluate) - - public virtual void OnPointerDown(PointerEventData eventData) - { - if (eventData.button != PointerEventData.InputButton.Left) - return; - - if (IsInteractable() && navigation.mode != UXNavigation.Mode.None && EventSystem.current != null) - EventSystem.current.SetSelectedGameObject(gameObject, eventData); - - isPointerDown = true; - EvaluateAndTransitionToSelectionState(); - } - - public virtual void OnPointerUp(PointerEventData eventData) - { - if (eventData.button != PointerEventData.InputButton.Left) - return; - - isPointerDown = false; - EvaluateAndTransitionToSelectionState(); - } - - public virtual void OnPointerEnter(PointerEventData eventData) - { - isPointerInside = true; - EvaluateAndTransitionToSelectionState(); - } - - public virtual void OnPointerExit(PointerEventData eventData) - { - isPointerInside = false; - EvaluateAndTransitionToSelectionState(); - } - - public virtual void OnSelect(BaseEventData eventData) - { - hasSelection = true; - EvaluateAndTransitionToSelectionState(); - } - - public virtual void OnDeselect(BaseEventData eventData) - { - hasSelection = false; - EvaluateAndTransitionToSelectionState(); - } - - #endregion - - #region Visual transition helpers (main transition + low level ops) - - /// - /// High-level API: apply main transition visuals (child classes can override, UXButton overrides to add child transitions). - /// - /// - /// - public virtual void ApplyVisualState(SelectionState state, bool instant) - { - if (m_MainTransition != null) - ApplyTransition(m_MainTransition, state, instant); - } - - /// - /// Apply a single TransitionData (handles ColorTint / SpriteSwap / Animation). - /// Animation triggering calls PlayAnimation (virtual). - /// - protected void ApplyTransition(TransitionData data, SelectionState state, bool instant) - { - if (data == null) return; - if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation) - return; - - Color color = Color.white; - Sprite sprite = null; - string trigger = null; - - switch (state) - { - case SelectionState.Normal: - color = data.colors.normalColor; - sprite = data.spriteState.highlightedSprite; - trigger = data.animationTriggers.normalTrigger; - break; - case SelectionState.Highlighted: - color = data.colors.highlightedColor; - sprite = data.spriteState.highlightedSprite; - trigger = data.animationTriggers.highlightedTrigger; - break; - case SelectionState.Pressed: - color = data.colors.pressedColor; - sprite = data.spriteState.pressedSprite; - trigger = data.animationTriggers.pressedTrigger; - break; - case SelectionState.Selected: - color = data.colors.selectedColor; - sprite = data.spriteState.selectedSprite; - trigger = data.animationTriggers.selectedTrigger; - break; - case SelectionState.Disabled: - color = data.colors.disabledColor; - sprite = data.spriteState.disabledSprite; - trigger = data.animationTriggers.disabledTrigger; - break; - } - - switch (data.transition) - { - 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; - } - } - - protected void TweenColor(TransitionData data, Color color, bool instant) - { - if (data == null || data.targetGraphic == null) return; - data.targetGraphic.CrossFadeColor( - color, - instant ? 0f : data.colors.fadeDuration, - true, - true - ); - } - - protected static void SwapSprite(TransitionData data, Sprite sprite) - { - if (data == null) return; - if (data.targetGraphic is Image img) - img.overrideSprite = sprite; - } - - /// - /// Subclasses override this to trigger Animator triggers, etc. - /// - /// - protected virtual void PlayAnimation(string trigger) - { - if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger) || !gameObject.activeInHierarchy) - return; - - foreach (int id in _animTriggerCache.Values) - Animator.ResetTrigger(id); - - if (_animTriggerCache.TryGetValue(trigger, out int hash)) - Animator.SetTrigger(hash); - } - - #endregion } } diff --git a/Runtime/UXComponent/UXSlider/UXSlider.cs b/Runtime/UXComponent/UXSlider/UXSlider.cs index 9332f2f..e37c96e 100644 --- a/Runtime/UXComponent/UXSlider/UXSlider.cs +++ b/Runtime/UXComponent/UXSlider/UXSlider.cs @@ -1,603 +1,603 @@ -using AlicizaX.UI.Extension; -using UnityEngine.EventSystems; -using Direction = UnityEngine.UI.Slider.Direction; - -namespace UnityEngine.UI -{ - [ExecuteAlways] - [RequireComponent(typeof(RectTransform))] - public class UXSlider : UXSelectable, IDragHandler, IInitializePotentialDragHandler, ICanvasElement - { - [SerializeField] private RectTransform m_FillRect; - - public RectTransform fillRect - { - get { return m_FillRect; } - set - { - if (SetPropertyUtility.SetClass(ref m_FillRect, value)) - { - UpdateCachedReferences(); - UpdateVisuals(); - } - } - } - - [SerializeField] private RectTransform m_HandleRect; - - public RectTransform handleRect - { - get { return m_HandleRect; } - set - { - if (SetPropertyUtility.SetClass(ref m_HandleRect, value)) - { - UpdateCachedReferences(); - UpdateVisuals(); - } - } - } - - [Space] [SerializeField] private Slider.Direction m_Direction = Slider.Direction.LeftToRight; - - public Slider.Direction direction - { - get { return m_Direction; } - set - { - if (SetPropertyUtility.SetStruct(ref m_Direction, value)) UpdateVisuals(); - } - } - - [SerializeField] private float m_MinValue = 0; - - public float minValue - { - get { return m_MinValue; } - set - { - if (SetPropertyUtility.SetStruct(ref m_MinValue, value)) - { - Set(m_Value); - UpdateVisuals(); - } - } - } - - [SerializeField] private float m_MaxValue = 1; - - public float maxValue - { - get { return m_MaxValue; } - set - { - if (SetPropertyUtility.SetStruct(ref m_MaxValue, value)) - { - Set(m_Value); - UpdateVisuals(); - } - } - } - - [SerializeField] private bool m_WholeNumbers = false; - - public bool wholeNumbers - { - get { return m_WholeNumbers; } - set - { - if (SetPropertyUtility.SetStruct(ref m_WholeNumbers, value)) - { - Set(m_Value); - UpdateVisuals(); - } - } - } - - [Space] [SerializeField] protected float m_Value; - - public virtual float value - { - get { return wholeNumbers ? Mathf.Round(m_Value) : m_Value; } - set { Set(value); } - } - - public virtual void SetValueWithoutNotify(float input) - { - Set(input, false); - } - - public float normalizedValue - { - get - { - if (Mathf.Approximately(minValue, maxValue)) - return 0; - return Mathf.InverseLerp(minValue, maxValue, value); - } - set { this.value = Mathf.Lerp(minValue, maxValue, value); } - } - - [Space] [SerializeField] private Slider.SliderEvent m_OnValueChanged = new Slider.SliderEvent(); - - public Slider.SliderEvent onValueChanged - { - get { return m_OnValueChanged; } - set { m_OnValueChanged = value; } - } - - // --- SMOOTHING FIELDS --- - [Space] - [SerializeField] private bool m_SmoothMovement = false; - [SerializeField] private float m_SmoothSpeed = 8f; // value-per-second, larger=faster - - public bool smoothMovement - { - get { return m_SmoothMovement; } - set { m_SmoothMovement = value; } - } - - /// - /// 平滑移动目标值(只有当 smoothMovement && !wholeNumbers 时生效) - /// - protected float m_TargetValue; - protected bool m_IsSmoothMoving = false; - // ------------------------ - - private Image m_FillImage; - private Transform m_FillTransform; - private RectTransform m_FillContainerRect; - private Transform m_HandleTransform; - private RectTransform m_HandleContainerRect; - - private Vector2 m_Offset = Vector2.zero; - -#pragma warning disable 649 - private DrivenRectTransformTracker m_Tracker; -#pragma warning restore 649 - - private bool m_DelayedUpdateVisuals = false; - - float stepSize - { - get { return wholeNumbers ? 1 : (maxValue - minValue) * 0.1f; } - } - - protected UXSlider() - { - } -#if UNITY_EDITOR - protected override void OnValidate() - { - base.OnValidate(); - if (wholeNumbers) - { - m_MinValue = Mathf.Round(m_MinValue); - m_MaxValue = Mathf.Round(m_MaxValue); - } - - //Onvalidate is called before OnEnabled. We need to make sure not to touch any other objects before OnEnable is run. - if (IsActive()) - { - UpdateCachedReferences(); - // Update rects in next update since other things might affect them even if value didn't change. - m_DelayedUpdateVisuals = true; - } - - if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying) - CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this); - } -#endif - - protected override void OnEnable() - { - base.OnEnable(); - UpdateCachedReferences(); - Set(m_Value, false); - // Update rects since they need to be initialized correctly. - UpdateVisuals(); - // 初始化 target - m_TargetValue = m_Value; - m_IsSmoothMoving = false; - } - - protected override void OnDisable() - { - m_Tracker.Clear(); - base.OnDisable(); - } - - protected virtual void Update() - { - if (m_DelayedUpdateVisuals) - { - m_DelayedUpdateVisuals = false; - Set(m_Value, false); - UpdateVisuals(); - } - - // 处理平滑移动(仅当开启时) - if (m_IsSmoothMoving) - { - // 如果 wholeNumbers 为 true,则取消平滑,直接设置最终值(避免小数) - if (wholeNumbers) - { - m_IsSmoothMoving = false; - Set(m_TargetValue); - } - else - { - float maxDelta = m_SmoothSpeed * Time.unscaledDeltaTime; - float newValue = Mathf.MoveTowards(m_Value, m_TargetValue, maxDelta); - // 使用 Set 来保持更新视觉并触发回调(会在数值改变时触发) - Set(newValue); - if (Mathf.Approximately(newValue, m_TargetValue)) - { - m_IsSmoothMoving = false; - // 确保最终值精确到目标(避免浮点残留) - Set(m_TargetValue); - } - } - } - } - - protected override void OnDidApplyAnimationProperties() - { - m_Value = ClampValue(m_Value); - float oldNormalizedValue = normalizedValue; - if (m_FillContainerRect != null) - { - if (m_FillImage != null && m_FillImage.type == Image.Type.Filled) - oldNormalizedValue = m_FillImage.fillAmount; - else - oldNormalizedValue = (reverseValue ? 1 - m_FillRect.anchorMin[(int)axis] : m_FillRect.anchorMax[(int)axis]); - } - else if (m_HandleContainerRect != null) - oldNormalizedValue = (reverseValue ? 1 - m_HandleRect.anchorMin[(int)axis] : m_HandleRect.anchorMin[(int)axis]); - - UpdateVisuals(); - - if (oldNormalizedValue != normalizedValue) - { - UISystemProfilerApi.AddMarker("Slider.value", this); - onValueChanged.Invoke(m_Value); - } - - // UUM-34170 Apparently, some properties on slider such as IsInteractable and Normalcolor Animation is broken. - // We need to call base here to render the animation on Scene - base.OnDidApplyAnimationProperties(); - } - - void UpdateCachedReferences() - { - if (m_FillRect && m_FillRect != (RectTransform)transform) - { - m_FillTransform = m_FillRect.transform; - m_FillImage = m_FillRect.GetComponent(); - if (m_FillTransform.parent != null) - m_FillContainerRect = m_FillTransform.parent.GetComponent(); - } - else - { - m_FillRect = null; - m_FillContainerRect = null; - m_FillImage = null; - } - - if (m_HandleRect && m_HandleRect != (RectTransform)transform) - { - m_HandleTransform = m_HandleRect.transform; - if (m_HandleTransform.parent != null) - m_HandleContainerRect = m_HandleTransform.parent.GetComponent(); - } - else - { - m_HandleRect = null; - m_HandleContainerRect = null; - } - } - - float ClampValue(float input) - { - float newValue = Mathf.Clamp(input, minValue, maxValue); - if (wholeNumbers) - newValue = Mathf.Round(newValue); - return newValue; - } - - protected virtual void Set(float input, bool sendCallback = true) - { - // Clamp the input - float newValue = ClampValue(input); - - // If the stepped value doesn't match the last one, it's time to update - if (m_Value == newValue) - return; - - m_Value = newValue; - UpdateVisuals(); - if (sendCallback) - { - UISystemProfilerApi.AddMarker("Slider.value", this); - m_OnValueChanged.Invoke(newValue); - } - } - - protected override void OnRectTransformDimensionsChange() - { - base.OnRectTransformDimensionsChange(); - if (!IsActive()) - return; - - UpdateVisuals(); - } - - enum Axis - { - Horizontal = 0, - Vertical = 1 - } - - Axis axis - { - get { return (m_Direction == Direction.LeftToRight || m_Direction == Direction.RightToLeft) ? Axis.Horizontal : Axis.Vertical; } - } - - bool reverseValue - { - get { return m_Direction == Direction.RightToLeft || m_Direction == Direction.TopToBottom; } - } - - - private void UpdateVisuals() - { -#if UNITY_EDITOR - if (!Application.isPlaying) - UpdateCachedReferences(); -#endif - - m_Tracker.Clear(); - - if (m_FillContainerRect != null) - { - m_Tracker.Add(this, m_FillRect, DrivenTransformProperties.Anchors); - Vector2 anchorMin = Vector2.zero; - Vector2 anchorMax = Vector2.one; - - if (m_FillImage != null && m_FillImage.type == Image.Type.Filled) - { - m_FillImage.fillAmount = normalizedValue; - } - else - { - if (reverseValue) - anchorMin[(int)axis] = 1 - normalizedValue; - else - anchorMax[(int)axis] = normalizedValue; - } - - m_FillRect.anchorMin = anchorMin; - m_FillRect.anchorMax = anchorMax; - } - - if (m_HandleContainerRect != null) - { - m_Tracker.Add(this, m_HandleRect, DrivenTransformProperties.Anchors); - Vector2 anchorMin = Vector2.zero; - Vector2 anchorMax = Vector2.one; - anchorMin[(int)axis] = anchorMax[(int)axis] = (reverseValue ? (1 - normalizedValue) : normalizedValue); - m_HandleRect.anchorMin = anchorMin; - m_HandleRect.anchorMax = anchorMax; - } - } - - void UpdateDrag(PointerEventData eventData, Camera cam) - { - RectTransform clickRect = m_HandleContainerRect ?? m_FillContainerRect; - if (clickRect != null && clickRect.rect.size[(int)axis] > 0) - { - Vector2 position = Vector2.zero; - if (!MultipleDisplayUtilities.GetRelativeMousePositionForDrag(eventData, ref position)) - return; - - Vector2 localCursor; - if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, position, cam, out localCursor)) - return; - localCursor -= clickRect.rect.position; - - float val = Mathf.Clamp01((localCursor - m_Offset)[(int)axis] / clickRect.rect.size[(int)axis]); - normalizedValue = (reverseValue ? 1f - val : val); - } - } - - private bool MayDrag(PointerEventData eventData) - { - return IsActive() && IsInteractable() && eventData.button == PointerEventData.InputButton.Left; - } - - public override void OnPointerDown(PointerEventData eventData) - { - if (!MayDrag(eventData)) - return; - - base.OnPointerDown(eventData); - - // 取消任何平滑移动(开始拖拽时) - m_IsSmoothMoving = false; - m_TargetValue = m_Value; - - m_Offset = Vector2.zero; - if (m_HandleContainerRect != null && RectTransformUtility.RectangleContainsScreenPoint(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.enterEventCamera)) - { - Vector2 localMousePos; - if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.pressEventCamera, out localMousePos)) - m_Offset = localMousePos; - } - else - { - // Outside the slider handle - jump to this point instead - UpdateDrag(eventData, eventData.pressEventCamera); - } - } - - public void OnDrag(PointerEventData eventData) - { - if (!MayDrag(eventData)) - return; - // 拖拽也取消平滑 - m_IsSmoothMoving = false; - m_TargetValue = m_Value; - UpdateDrag(eventData, eventData.pressEventCamera); - } - - public override void OnMove(AxisEventData eventData) - { - if (!IsActive() || !IsInteractable()) - { - base.OnMove(eventData); - return; - } - - // 基准值:如果当前已经在平滑移动中,累加到 target 上;否则从当前 value 开始。 - float currentBase = m_IsSmoothMoving ? m_TargetValue : value; - - switch (eventData.moveDir) - { - case MoveDirection.Left: - if (axis == Axis.Horizontal && FindSelectableOnLeft() == null) - { - float dest = ClampValue(reverseValue ? currentBase + stepSize : currentBase - stepSize); - if (m_SmoothMovement && !wholeNumbers) - { - m_TargetValue = dest; - m_IsSmoothMoving = true; - } - else - { - Set(dest); - } - } - else - base.OnMove(eventData); - break; - case MoveDirection.Right: - if (axis == Axis.Horizontal && FindSelectableOnRight() == null) - { - float dest = ClampValue(reverseValue ? currentBase - stepSize : currentBase + stepSize); - if (m_SmoothMovement && !wholeNumbers) - { - m_TargetValue = dest; - m_IsSmoothMoving = true; - } - else - { - Set(dest); - } - } - else - base.OnMove(eventData); - break; - case MoveDirection.Up: - if (axis == Axis.Vertical && FindSelectableOnUp() == null) - { - float dest = ClampValue(reverseValue ? currentBase - stepSize : currentBase + stepSize); - if (m_SmoothMovement && !wholeNumbers) - { - m_TargetValue = dest; - m_IsSmoothMoving = true; - } - else - { - Set(dest); - } - } - else - base.OnMove(eventData); - break; - case MoveDirection.Down: - if (axis == Axis.Vertical && FindSelectableOnDown() == null) - { - float dest = ClampValue(reverseValue ? currentBase + stepSize : currentBase - stepSize); - if (m_SmoothMovement && !wholeNumbers) - { - m_TargetValue = dest; - m_IsSmoothMoving = true; - } - else - { - Set(dest); - } - } - else - base.OnMove(eventData); - break; - } - } - - public override UXSelectable FindSelectableOnLeft() - { - if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Horizontal) - return null; - return base.FindSelectableOnLeft(); - } - - public override UXSelectable FindSelectableOnRight() - { - if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Horizontal) - return null; - return base.FindSelectableOnRight(); - } - - public override UXSelectable FindSelectableOnUp() - { - if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Vertical) - return null; - return base.FindSelectableOnUp(); - } - - public override UXSelectable FindSelectableOnDown() - { - if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Vertical) - return null; - return base.FindSelectableOnDown(); - } - - - public void OnInitializePotentialDrag(PointerEventData eventData) - { - eventData.useDragThreshold = false; - } - - public void SetDirection(Direction direction, bool includeRectLayouts) - { - Axis oldAxis = axis; - bool oldReverse = reverseValue; - this.direction = direction; - - if (!includeRectLayouts) - return; - - if (axis != oldAxis) - RectTransformUtility.FlipLayoutAxes(transform as RectTransform, true, true); - - if (reverseValue != oldReverse) - RectTransformUtility.FlipLayoutOnAxis(transform as RectTransform, (int)axis, true, true); - } - - public void Rebuild(CanvasUpdate executing) - { -#if UNITY_EDITOR - if (executing == CanvasUpdate.Prelayout) - onValueChanged.Invoke(value); -#endif - } - - public void LayoutComplete() - { - } - - public void GraphicUpdateComplete() - { - } - } -} +// using AlicizaX.UI.Extension; +// using UnityEngine.EventSystems; +// using Direction = UnityEngine.UI.Slider.Direction; +// +// namespace UnityEngine.UI +// { +// [ExecuteAlways] +// [RequireComponent(typeof(RectTransform))] +// public class UXSlider : UXSelectable, IDragHandler, IInitializePotentialDragHandler, ICanvasElement +// { +// [SerializeField] private RectTransform m_FillRect; +// +// public RectTransform fillRect +// { +// get { return m_FillRect; } +// set +// { +// if (SetPropertyUtility.SetClass(ref m_FillRect, value)) +// { +// UpdateCachedReferences(); +// UpdateVisuals(); +// } +// } +// } +// +// [SerializeField] private RectTransform m_HandleRect; +// +// public RectTransform handleRect +// { +// get { return m_HandleRect; } +// set +// { +// if (SetPropertyUtility.SetClass(ref m_HandleRect, value)) +// { +// UpdateCachedReferences(); +// UpdateVisuals(); +// } +// } +// } +// +// [Space] [SerializeField] private Slider.Direction m_Direction = Slider.Direction.LeftToRight; +// +// public Slider.Direction direction +// { +// get { return m_Direction; } +// set +// { +// if (SetPropertyUtility.SetStruct(ref m_Direction, value)) UpdateVisuals(); +// } +// } +// +// [SerializeField] private float m_MinValue = 0; +// +// public float minValue +// { +// get { return m_MinValue; } +// set +// { +// if (SetPropertyUtility.SetStruct(ref m_MinValue, value)) +// { +// Set(m_Value); +// UpdateVisuals(); +// } +// } +// } +// +// [SerializeField] private float m_MaxValue = 1; +// +// public float maxValue +// { +// get { return m_MaxValue; } +// set +// { +// if (SetPropertyUtility.SetStruct(ref m_MaxValue, value)) +// { +// Set(m_Value); +// UpdateVisuals(); +// } +// } +// } +// +// [SerializeField] private bool m_WholeNumbers = false; +// +// public bool wholeNumbers +// { +// get { return m_WholeNumbers; } +// set +// { +// if (SetPropertyUtility.SetStruct(ref m_WholeNumbers, value)) +// { +// Set(m_Value); +// UpdateVisuals(); +// } +// } +// } +// +// [Space] [SerializeField] protected float m_Value; +// +// public virtual float value +// { +// get { return wholeNumbers ? Mathf.Round(m_Value) : m_Value; } +// set { Set(value); } +// } +// +// public virtual void SetValueWithoutNotify(float input) +// { +// Set(input, false); +// } +// +// public float normalizedValue +// { +// get +// { +// if (Mathf.Approximately(minValue, maxValue)) +// return 0; +// return Mathf.InverseLerp(minValue, maxValue, value); +// } +// set { this.value = Mathf.Lerp(minValue, maxValue, value); } +// } +// +// [Space] [SerializeField] private Slider.SliderEvent m_OnValueChanged = new Slider.SliderEvent(); +// +// public Slider.SliderEvent onValueChanged +// { +// get { return m_OnValueChanged; } +// set { m_OnValueChanged = value; } +// } +// +// // --- SMOOTHING FIELDS --- +// [Space] +// [SerializeField] private bool m_SmoothMovement = false; +// [SerializeField] private float m_SmoothSpeed = 8f; // value-per-second, larger=faster +// +// public bool smoothMovement +// { +// get { return m_SmoothMovement; } +// set { m_SmoothMovement = value; } +// } +// +// /// +// /// 平滑移动目标值(只有当 smoothMovement && !wholeNumbers 时生效) +// /// +// protected float m_TargetValue; +// protected bool m_IsSmoothMoving = false; +// // ------------------------ +// +// private Image m_FillImage; +// private Transform m_FillTransform; +// private RectTransform m_FillContainerRect; +// private Transform m_HandleTransform; +// private RectTransform m_HandleContainerRect; +// +// private Vector2 m_Offset = Vector2.zero; +// +// #pragma warning disable 649 +// private DrivenRectTransformTracker m_Tracker; +// #pragma warning restore 649 +// +// private bool m_DelayedUpdateVisuals = false; +// +// float stepSize +// { +// get { return wholeNumbers ? 1 : (maxValue - minValue) * 0.1f; } +// } +// +// protected UXSlider() +// { +// } +// #if UNITY_EDITOR +// protected override void OnValidate() +// { +// base.OnValidate(); +// if (wholeNumbers) +// { +// m_MinValue = Mathf.Round(m_MinValue); +// m_MaxValue = Mathf.Round(m_MaxValue); +// } +// +// //Onvalidate is called before OnEnabled. We need to make sure not to touch any other objects before OnEnable is run. +// if (IsActive()) +// { +// UpdateCachedReferences(); +// // Update rects in next update since other things might affect them even if value didn't change. +// m_DelayedUpdateVisuals = true; +// } +// +// if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying) +// CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this); +// } +// #endif +// +// protected override void OnEnable() +// { +// base.OnEnable(); +// UpdateCachedReferences(); +// Set(m_Value, false); +// // Update rects since they need to be initialized correctly. +// UpdateVisuals(); +// // 初始化 target +// m_TargetValue = m_Value; +// m_IsSmoothMoving = false; +// } +// +// protected override void OnDisable() +// { +// m_Tracker.Clear(); +// base.OnDisable(); +// } +// +// protected virtual void Update() +// { +// if (m_DelayedUpdateVisuals) +// { +// m_DelayedUpdateVisuals = false; +// Set(m_Value, false); +// UpdateVisuals(); +// } +// +// // 处理平滑移动(仅当开启时) +// if (m_IsSmoothMoving) +// { +// // 如果 wholeNumbers 为 true,则取消平滑,直接设置最终值(避免小数) +// if (wholeNumbers) +// { +// m_IsSmoothMoving = false; +// Set(m_TargetValue); +// } +// else +// { +// float maxDelta = m_SmoothSpeed * Time.unscaledDeltaTime; +// float newValue = Mathf.MoveTowards(m_Value, m_TargetValue, maxDelta); +// // 使用 Set 来保持更新视觉并触发回调(会在数值改变时触发) +// Set(newValue); +// if (Mathf.Approximately(newValue, m_TargetValue)) +// { +// m_IsSmoothMoving = false; +// // 确保最终值精确到目标(避免浮点残留) +// Set(m_TargetValue); +// } +// } +// } +// } +// +// protected override void OnDidApplyAnimationProperties() +// { +// m_Value = ClampValue(m_Value); +// float oldNormalizedValue = normalizedValue; +// if (m_FillContainerRect != null) +// { +// if (m_FillImage != null && m_FillImage.type == Image.Type.Filled) +// oldNormalizedValue = m_FillImage.fillAmount; +// else +// oldNormalizedValue = (reverseValue ? 1 - m_FillRect.anchorMin[(int)axis] : m_FillRect.anchorMax[(int)axis]); +// } +// else if (m_HandleContainerRect != null) +// oldNormalizedValue = (reverseValue ? 1 - m_HandleRect.anchorMin[(int)axis] : m_HandleRect.anchorMin[(int)axis]); +// +// UpdateVisuals(); +// +// if (oldNormalizedValue != normalizedValue) +// { +// UISystemProfilerApi.AddMarker("Slider.value", this); +// onValueChanged.Invoke(m_Value); +// } +// +// // UUM-34170 Apparently, some properties on slider such as IsInteractable and Normalcolor Animation is broken. +// // We need to call base here to render the animation on Scene +// base.OnDidApplyAnimationProperties(); +// } +// +// void UpdateCachedReferences() +// { +// if (m_FillRect && m_FillRect != (RectTransform)transform) +// { +// m_FillTransform = m_FillRect.transform; +// m_FillImage = m_FillRect.GetComponent(); +// if (m_FillTransform.parent != null) +// m_FillContainerRect = m_FillTransform.parent.GetComponent(); +// } +// else +// { +// m_FillRect = null; +// m_FillContainerRect = null; +// m_FillImage = null; +// } +// +// if (m_HandleRect && m_HandleRect != (RectTransform)transform) +// { +// m_HandleTransform = m_HandleRect.transform; +// if (m_HandleTransform.parent != null) +// m_HandleContainerRect = m_HandleTransform.parent.GetComponent(); +// } +// else +// { +// m_HandleRect = null; +// m_HandleContainerRect = null; +// } +// } +// +// float ClampValue(float input) +// { +// float newValue = Mathf.Clamp(input, minValue, maxValue); +// if (wholeNumbers) +// newValue = Mathf.Round(newValue); +// return newValue; +// } +// +// protected virtual void Set(float input, bool sendCallback = true) +// { +// // Clamp the input +// float newValue = ClampValue(input); +// +// // If the stepped value doesn't match the last one, it's time to update +// if (m_Value == newValue) +// return; +// +// m_Value = newValue; +// UpdateVisuals(); +// if (sendCallback) +// { +// UISystemProfilerApi.AddMarker("Slider.value", this); +// m_OnValueChanged.Invoke(newValue); +// } +// } +// +// protected override void OnRectTransformDimensionsChange() +// { +// base.OnRectTransformDimensionsChange(); +// if (!IsActive()) +// return; +// +// UpdateVisuals(); +// } +// +// enum Axis +// { +// Horizontal = 0, +// Vertical = 1 +// } +// +// Axis axis +// { +// get { return (m_Direction == Direction.LeftToRight || m_Direction == Direction.RightToLeft) ? Axis.Horizontal : Axis.Vertical; } +// } +// +// bool reverseValue +// { +// get { return m_Direction == Direction.RightToLeft || m_Direction == Direction.TopToBottom; } +// } +// +// +// private void UpdateVisuals() +// { +// #if UNITY_EDITOR +// if (!Application.isPlaying) +// UpdateCachedReferences(); +// #endif +// +// m_Tracker.Clear(); +// +// if (m_FillContainerRect != null) +// { +// m_Tracker.Add(this, m_FillRect, DrivenTransformProperties.Anchors); +// Vector2 anchorMin = Vector2.zero; +// Vector2 anchorMax = Vector2.one; +// +// if (m_FillImage != null && m_FillImage.type == Image.Type.Filled) +// { +// m_FillImage.fillAmount = normalizedValue; +// } +// else +// { +// if (reverseValue) +// anchorMin[(int)axis] = 1 - normalizedValue; +// else +// anchorMax[(int)axis] = normalizedValue; +// } +// +// m_FillRect.anchorMin = anchorMin; +// m_FillRect.anchorMax = anchorMax; +// } +// +// if (m_HandleContainerRect != null) +// { +// m_Tracker.Add(this, m_HandleRect, DrivenTransformProperties.Anchors); +// Vector2 anchorMin = Vector2.zero; +// Vector2 anchorMax = Vector2.one; +// anchorMin[(int)axis] = anchorMax[(int)axis] = (reverseValue ? (1 - normalizedValue) : normalizedValue); +// m_HandleRect.anchorMin = anchorMin; +// m_HandleRect.anchorMax = anchorMax; +// } +// } +// +// void UpdateDrag(PointerEventData eventData, Camera cam) +// { +// RectTransform clickRect = m_HandleContainerRect ?? m_FillContainerRect; +// if (clickRect != null && clickRect.rect.size[(int)axis] > 0) +// { +// Vector2 position = Vector2.zero; +// if (!MultipleDisplayUtilities.GetRelativeMousePositionForDrag(eventData, ref position)) +// return; +// +// Vector2 localCursor; +// if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, position, cam, out localCursor)) +// return; +// localCursor -= clickRect.rect.position; +// +// float val = Mathf.Clamp01((localCursor - m_Offset)[(int)axis] / clickRect.rect.size[(int)axis]); +// normalizedValue = (reverseValue ? 1f - val : val); +// } +// } +// +// private bool MayDrag(PointerEventData eventData) +// { +// return IsActive() && IsInteractable() && eventData.button == PointerEventData.InputButton.Left; +// } +// +// public override void OnPointerDown(PointerEventData eventData) +// { +// if (!MayDrag(eventData)) +// return; +// +// base.OnPointerDown(eventData); +// +// // 取消任何平滑移动(开始拖拽时) +// m_IsSmoothMoving = false; +// m_TargetValue = m_Value; +// +// m_Offset = Vector2.zero; +// if (m_HandleContainerRect != null && RectTransformUtility.RectangleContainsScreenPoint(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.enterEventCamera)) +// { +// Vector2 localMousePos; +// if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.pressEventCamera, out localMousePos)) +// m_Offset = localMousePos; +// } +// else +// { +// // Outside the slider handle - jump to this point instead +// UpdateDrag(eventData, eventData.pressEventCamera); +// } +// } +// +// public void OnDrag(PointerEventData eventData) +// { +// if (!MayDrag(eventData)) +// return; +// // 拖拽也取消平滑 +// m_IsSmoothMoving = false; +// m_TargetValue = m_Value; +// UpdateDrag(eventData, eventData.pressEventCamera); +// } +// +// public override void OnMove(AxisEventData eventData) +// { +// if (!IsActive() || !IsInteractable()) +// { +// base.OnMove(eventData); +// return; +// } +// +// // 基准值:如果当前已经在平滑移动中,累加到 target 上;否则从当前 value 开始。 +// float currentBase = m_IsSmoothMoving ? m_TargetValue : value; +// +// switch (eventData.moveDir) +// { +// case MoveDirection.Left: +// if (axis == Axis.Horizontal && FindSelectableOnLeft() == null) +// { +// float dest = ClampValue(reverseValue ? currentBase + stepSize : currentBase - stepSize); +// if (m_SmoothMovement && !wholeNumbers) +// { +// m_TargetValue = dest; +// m_IsSmoothMoving = true; +// } +// else +// { +// Set(dest); +// } +// } +// else +// base.OnMove(eventData); +// break; +// case MoveDirection.Right: +// if (axis == Axis.Horizontal && FindSelectableOnRight() == null) +// { +// float dest = ClampValue(reverseValue ? currentBase - stepSize : currentBase + stepSize); +// if (m_SmoothMovement && !wholeNumbers) +// { +// m_TargetValue = dest; +// m_IsSmoothMoving = true; +// } +// else +// { +// Set(dest); +// } +// } +// else +// base.OnMove(eventData); +// break; +// case MoveDirection.Up: +// if (axis == Axis.Vertical && FindSelectableOnUp() == null) +// { +// float dest = ClampValue(reverseValue ? currentBase - stepSize : currentBase + stepSize); +// if (m_SmoothMovement && !wholeNumbers) +// { +// m_TargetValue = dest; +// m_IsSmoothMoving = true; +// } +// else +// { +// Set(dest); +// } +// } +// else +// base.OnMove(eventData); +// break; +// case MoveDirection.Down: +// if (axis == Axis.Vertical && FindSelectableOnDown() == null) +// { +// float dest = ClampValue(reverseValue ? currentBase + stepSize : currentBase - stepSize); +// if (m_SmoothMovement && !wholeNumbers) +// { +// m_TargetValue = dest; +// m_IsSmoothMoving = true; +// } +// else +// { +// Set(dest); +// } +// } +// else +// base.OnMove(eventData); +// break; +// } +// } +// +// public override UXSelectable FindSelectableOnLeft() +// { +// if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Horizontal) +// return null; +// return base.FindSelectableOnLeft(); +// } +// +// public override UXSelectable FindSelectableOnRight() +// { +// if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Horizontal) +// return null; +// return base.FindSelectableOnRight(); +// } +// +// public override UXSelectable FindSelectableOnUp() +// { +// if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Vertical) +// return null; +// return base.FindSelectableOnUp(); +// } +// +// public override UXSelectable FindSelectableOnDown() +// { +// if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Vertical) +// return null; +// return base.FindSelectableOnDown(); +// } +// +// +// public void OnInitializePotentialDrag(PointerEventData eventData) +// { +// eventData.useDragThreshold = false; +// } +// +// public void SetDirection(Direction direction, bool includeRectLayouts) +// { +// Axis oldAxis = axis; +// bool oldReverse = reverseValue; +// this.direction = direction; +// +// if (!includeRectLayouts) +// return; +// +// if (axis != oldAxis) +// RectTransformUtility.FlipLayoutAxes(transform as RectTransform, true, true); +// +// if (reverseValue != oldReverse) +// RectTransformUtility.FlipLayoutOnAxis(transform as RectTransform, (int)axis, true, true); +// } +// +// public void Rebuild(CanvasUpdate executing) +// { +// #if UNITY_EDITOR +// if (executing == CanvasUpdate.Prelayout) +// onValueChanged.Invoke(value); +// #endif +// } +// +// public void LayoutComplete() +// { +// } +// +// public void GraphicUpdateComplete() +// { +// } +// } +// }