diff --git a/Editor/Helper/ExtensionHelper.cs b/Editor/Helper/ExtensionHelper.cs index 639d195..1a0e843 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.Editor +namespace AlicizaX.UI.Extension { internal static class ExtensionHelper { diff --git a/Editor/Helper/GUILayoutHelper.cs b/Editor/Helper/GUILayoutHelper.cs index cf2b3ad..c995846 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.Editor +namespace AlicizaX.UI.Extension { internal class GUILayoutHelper { diff --git a/Editor/Inspector/UXCreateHelper.cs b/Editor/Inspector/UXCreateHelper.cs index 9d739ae..e9ec77f 100644 --- a/Editor/Inspector/UXCreateHelper.cs +++ b/Editor/Inspector/UXCreateHelper.cs @@ -40,6 +40,22 @@ public class UXCreateHelper : Editor image.material = AssetDatabase.LoadAssetAtPath(UXGUIConfig.UIDefaultMatPath); } + + [MenuItem("GameObject/UI/UXSlider")] + public static void CreateUXSlider(MenuCommand menuCommand) + { + Type MenuOptionsType = typeof(UnityEditor.UI.SliderEditor).Assembly.GetType("UnityEditor.UI.MenuOptions"); + InvokeMethod(MenuOptionsType, "AddSlider", 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(); + } + #if TEXTMESHPRO_SUPPORT [MenuItem("GameObject/UI/UXTextMeshPro")] diff --git a/Editor/Res/ComponentIcon/Selectable Icon.png b/Editor/Res/ComponentIcon/Selectable Icon.png new file mode 100644 index 0000000..7371833 Binary files /dev/null and b/Editor/Res/ComponentIcon/Selectable Icon.png differ diff --git a/Editor/Res/ComponentIcon/Selectable Icon.png.meta b/Editor/Res/ComponentIcon/Selectable Icon.png.meta new file mode 100644 index 0000000..e9296c6 --- /dev/null +++ b/Editor/Res/ComponentIcon/Selectable Icon.png.meta @@ -0,0 +1,127 @@ +fileFormatVersion: 2 +guid: 1ab39039c9d7d844aa962517519f0ad6 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: WebGL + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Res/ComponentIcon/Slider Icon.png b/Editor/Res/ComponentIcon/Slider Icon.png new file mode 100644 index 0000000..e8c5240 Binary files /dev/null and b/Editor/Res/ComponentIcon/Slider Icon.png differ diff --git a/Editor/Res/ComponentIcon/Slider Icon.png.meta b/Editor/Res/ComponentIcon/Slider Icon.png.meta new file mode 100644 index 0000000..540eb5e --- /dev/null +++ b/Editor/Res/ComponentIcon/Slider Icon.png.meta @@ -0,0 +1,127 @@ +fileFormatVersion: 2 +guid: dfe8ade815753dc4d9ca3ce5d981cb91 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: WebGL + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UX/Button.meta b/Editor/UX/Button.meta new file mode 100644 index 0000000..470d6b4 --- /dev/null +++ b/Editor/UX/Button.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: baf1c03f83bf4c46aaf15b1cb5ff48cd +timeCreated: 1765184499 \ No newline at end of file diff --git a/Editor/UX/Button/UXButtonEditor.cs b/Editor/UX/Button/UXButtonEditor.cs new file mode 100644 index 0000000..bb9eafd --- /dev/null +++ b/Editor/UX/Button/UXButtonEditor.cs @@ -0,0 +1,402 @@ +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/UXButtonEditor.cs.meta b/Editor/UX/Button/UXButtonEditor.cs.meta similarity index 100% rename from Editor/UX/UXButtonEditor.cs.meta rename to Editor/UX/Button/UXButtonEditor.cs.meta diff --git a/Editor/UX/Group.meta b/Editor/UX/Group.meta new file mode 100644 index 0000000..9a36074 --- /dev/null +++ b/Editor/UX/Group.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 21c6f15c77fa4c6abbe65350c76028a4 +timeCreated: 1765184506 \ No newline at end of file diff --git a/Editor/UX/Group/UXGroupInspector.cs b/Editor/UX/Group/UXGroupInspector.cs new file mode 100644 index 0000000..7f53fad --- /dev/null +++ b/Editor/UX/Group/UXGroupInspector.cs @@ -0,0 +1,153 @@ +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/UXGroupInspector.cs.meta b/Editor/UX/Group/UXGroupInspector.cs.meta similarity index 100% rename from Editor/UX/UXGroupInspector.cs.meta rename to Editor/UX/Group/UXGroupInspector.cs.meta diff --git a/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs b/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs index b653957..d211115 100644 --- a/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs +++ b/Editor/UX/Hotkey/HotkeyBindComponentInspector.cs @@ -8,7 +8,7 @@ using AlicizaX.UI.Runtime; using UnityEditor; using UnityEngine; -namespace UnityEngine.UI.Hotkey +namespace UnityEngine.UI { [CustomEditor(typeof(HotkeyBindComponent))] public class HotkeyBindComponentInspector : GameFrameworkInspector @@ -71,9 +71,9 @@ namespace UnityEngine.UI.Hotkey for (int i = 0; i < listProp.arraySize; i++) { var element = listProp.GetArrayElementAtIndex(i); - var uxHotkey = element.objectReferenceValue as UXHotkey; + var uxHotkey = element.objectReferenceValue as UXHotkeyButton; string name = uxHotkey != null ? uxHotkey.name : "Null"; - EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXHotkey), true); + EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXHotkeyButton), true); } EditorGUI.indentLevel--; @@ -86,8 +86,8 @@ namespace UnityEngine.UI.Hotkey { Undo.RecordObject(_target, "Scan UXHotkey"); - var uxHotkeys = _target.GetComponentsInChildren(true); - List valiedHotkeys = new List(); + var uxHotkeys = _target.GetComponentsInChildren(true); + List valiedHotkeys = new List(); foreach (var item in uxHotkeys) { var field = item.GetType() diff --git a/Editor/UX/Hotkey/HotkeyInspector.cs b/Editor/UX/Hotkey/HotkeyInspector.cs deleted file mode 100644 index 51a91b7..0000000 --- a/Editor/UX/Hotkey/HotkeyInspector.cs +++ /dev/null @@ -1,45 +0,0 @@ -#if INPUTSYSTEM_SUPPORT -using AlicizaX.Editor; -using AlicizaX.UI.Extension; -using UnityEditor; - -namespace UnityEngine.UI.Hotkey -{ - [CustomEditor(typeof(UXHotkey))] - public class HotkeyInspector : GameFrameworkInspector - { - private UXHotkey _target; - private SerializedProperty _button; - private SerializedProperty _hotKeyRefrence; - private SerializedProperty _hotkeyPressType; - - private void OnEnable() - { - _target = (UXHotkey)target; - _button = serializedObject.FindProperty("_button"); - _hotKeyRefrence = serializedObject.FindProperty("_hotKeyRefrence"); - _hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType"); - } - - public override void OnInspectorGUI() - { - serializedObject.Update(); - - var holder = _target.GetComponent(); - if (holder == null) - { - EditorGUILayout.HelpBox( - "⚠ 当前对象缺少 UXButton 组件", - MessageType.Error - ); - } - - EditorGUILayout.PropertyField(_button); - EditorGUILayout.PropertyField(_hotKeyRefrence); - EditorGUILayout.PropertyField(_hotkeyPressType); - serializedObject.ApplyModifiedProperties(); - } - } -} - -#endif diff --git a/Editor/UX/Hotkey/HotkeyInspector.cs.meta b/Editor/UX/Hotkey/HotkeyInspector.cs.meta deleted file mode 100644 index 8a4c8a0..0000000 --- a/Editor/UX/Hotkey/HotkeyInspector.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 87a10dc3a1544ec586ff27809213a127 -timeCreated: 1760341800 \ No newline at end of file diff --git a/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs b/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs new file mode 100644 index 0000000..f10636a --- /dev/null +++ b/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs @@ -0,0 +1,31 @@ +using AlicizaX.Editor; +using AlicizaX.UI.Extension; +using UnityEditor; + +namespace UnityEngine.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")); + } + } + } +} diff --git a/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta b/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta new file mode 100644 index 0000000..aeb8405 --- /dev/null +++ b/Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dc0b010e4f884f6992de90c5e5dd7154 +timeCreated: 1765185454 \ No newline at end of file diff --git a/Editor/UX/Iamge.meta b/Editor/UX/Iamge.meta new file mode 100644 index 0000000..9c1b504 --- /dev/null +++ b/Editor/UX/Iamge.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 681126dbfd104770895c5b2dedb80aa0 +timeCreated: 1765184491 \ No newline at end of file diff --git a/Editor/UX/UXImageEditor.cs b/Editor/UX/Iamge/UXImageEditor.cs similarity index 100% rename from Editor/UX/UXImageEditor.cs rename to Editor/UX/Iamge/UXImageEditor.cs diff --git a/Editor/UX/UXImageEditor.cs.meta b/Editor/UX/Iamge/UXImageEditor.cs.meta similarity index 100% rename from Editor/UX/UXImageEditor.cs.meta rename to Editor/UX/Iamge/UXImageEditor.cs.meta diff --git a/Editor/UX/Selectable.meta b/Editor/UX/Selectable.meta new file mode 100644 index 0000000..d459263 --- /dev/null +++ b/Editor/UX/Selectable.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 29e9949ea8274c14b76e93d316a2d101 +timeCreated: 1765184529 \ No newline at end of file diff --git a/Editor/UX/Selectable/UXSelectableEditor.cs b/Editor/UX/Selectable/UXSelectableEditor.cs new file mode 100644 index 0000000..140c559 --- /dev/null +++ b/Editor/UX/Selectable/UXSelectableEditor.cs @@ -0,0 +1,570 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AlicizaX.UI.Extension; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.UI; +using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType; + +namespace UnityEngine.UI +{ + [CanEditMultipleObjects] + [CustomEditor(typeof(UXSelectable), true)] + internal class UXSelectableEditor : Editor + { + 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(); + + // 序列化属性(基类需要) + protected SerializedProperty m_Navigation; + protected SerializedProperty m_MainTransition; + protected SerializedProperty m_Interactable; + + // 抽象皮肤(现在放在基类) + protected GUISkin customSkin; + + // Tab 管理结构 + protected class EditorTab + { + public string title; + public string iconName; + public List callbacks = new List(); + + public EditorTab(string t, string icon) + { + title = t; + iconName = icon; + } + } + + private List _tabs = new List(); + private int _currentTabIndex = 0; + + protected virtual void OnEnable() + { + s_ShowNavigation = EditorPrefs.GetBool(s_ShowNavigationKey); + s_Editors.Add(this); + RegisterStaticOnSceneGUI(); + + m_Navigation = serializedObject.FindProperty("m_Navigation"); + m_MainTransition = serializedObject.FindProperty("m_MainTransition"); + m_Interactable = serializedObject.FindProperty("m_Interactable"); + // 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() + { + s_Editors.Remove(this); + RegisterStaticOnSceneGUI(); + _tabs.Clear(); + _currentTabIndex = 0; + } + + #region Tab API (Register / Append / Remove / Unregister) + + /// + /// 注册一个新 tab(如果已存在同名 tab,则替换其 icon 和清空回调,再加入 drawCallback) + /// + protected 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 EditorTab(title, iconName); + _tabs.Add(tab); + } + else + { + tab.iconName = iconName; + tab.callbacks.Clear(); + } + + if (drawCallback != null) + tab.callbacks.Add(drawCallback); + } + + /// + /// 在已存在 tab 后追加一个 callback;若 tab 不存在则创建并追加。 + /// + protected 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 EditorTab(title, "d_DefaultAsset Icon"); + _tabs.Add(tab); + } + + // 避免重复追加(简单判断) + 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); + } + + /// + /// 卸载整个 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() + { + Rect toggleRect = EditorGUILayout.GetControlRect(); + toggleRect.xMin += EditorGUIUtility.labelWidth; + EditorGUI.BeginChangeCheck(); + s_ShowNavigation = GUI.Toggle(toggleRect, s_ShowNavigation, m_VisualizeNavigation, EditorStyles.miniButton); + if (EditorGUI.EndChangeCheck()) + { + EditorPrefs.SetBool(s_ShowNavigationKey, s_ShowNavigation); + 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) + EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_Mode"), new GUIContent("Navigation")); + + 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; + // keep selection state consistent + 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; + if (graphic == null) + { + graphic = (target as UXSelectable).GetComponent(); + if (targetGraphic != null) + targetGraphic.objectReferenceValue = graphic; + } + + SerializedProperty transition = m_MainTransition.FindPropertyRelative("transition"); + var currentTransition = GetTransition(transition); + + switch (currentTransition) + { + case Selectable.Transition.ColorTint: + case Selectable.Transition.SpriteSwap: + EditorGUILayout.PropertyField(targetGraphic); + break; + } + + EditorGUILayout.PropertyField(transition); + + var animator = graphic != null ? graphic.GetComponent() : (target as UXSelectable).GetComponent(); + switch (currentTransition) + { + case Selectable.Transition.ColorTint: + if (graphic == null) + EditorGUILayout.HelpBox("需要Graphic组件来使用颜色过渡", MessageType.Warning); + 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); + 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(); + + UnityEditor.Animations.AnimatorController.SetAnimatorController(animator, controller); + } + } + } + + 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); + } + + EditorGUILayout.Space(); + GUILayout.EndVertical(); + } + + protected void CheckAndSetColorDefaults(SerializedProperty colorBlock, SerializedProperty targetGraphic) + { + if (colorBlock == null) return; + + bool isDirty = false; + string[] colorProps = new string[] { "m_NormalColor", "m_HighlightedColor", "m_PressedColor", "m_SelectedColor", "m_DisabledColor" }; + foreach (var propName in colorProps) + { + SerializedProperty prop = colorBlock.FindPropertyRelative(propName); + if (prop == null) continue; + Color color = prop.colorValue; + if (color.r == 0 && color.g == 0 && color.b == 0 && color.a == 0) + { + isDirty = true; + if (prop.name == "m_PressedColor") + { + prop.colorValue = new Color(0.7843137f, 0.7843137f, 0.7843137f, 1.0f); + } + else if (prop.name == "m_DisabledColor") + { + prop.colorValue = new Color(0.7843137f, 0.7843137f, 0.7843137f, 0.5f); + } + else + { + prop.colorValue = Color.white; + } + } + } + + SerializedProperty fadeDuration = colorBlock.FindPropertyRelative("m_FadeDuration"); + SerializedProperty m_ColorMultiplier = colorBlock.FindPropertyRelative("m_ColorMultiplier"); + if (isDirty) + { + if (m_ColorMultiplier != null) m_ColorMultiplier.floatValue = 1f; + if (fadeDuration != null) fadeDuration.floatValue = 0.1f; + } + + var graphic = targetGraphic != null ? targetGraphic.objectReferenceValue as Graphic : null; + if (graphic != null) + { + if (!EditorApplication.isPlaying) + { + SerializedProperty normalColorProp = colorBlock.FindPropertyRelative("m_NormalColor"); + if (normalColorProp != null) + { + Color color = normalColorProp.colorValue; + graphic.canvasRenderer.SetColor(color); + } + } + } + } + + protected static UnityEditor.Animations.AnimatorController GenerateSelectableAnimatorContoller(SerializedProperty property, UnityEngine.Object targetObj) + { + if (targetObj == null || property == null) + return null; + + var targetAsGO = (targetObj as UXSelectable); + var path = GetSaveControllerPath(targetAsGO); + 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; + + var controller = UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(path); + GenerateTriggerableTransition(normalName, controller); + GenerateTriggerableTransition(highlightedName, controller); + GenerateTriggerableTransition(pressedName, controller); + GenerateTriggerableTransition(selectedName, controller); + GenerateTriggerableTransition(disabledName, controller); + + AssetDatabase.ImportAsset(path); + + return controller; + } + + private static AnimationClip GenerateTriggerableTransition(string name, UnityEditor.Animations.AnimatorController controller) + { + // Create the clip + var clip = UnityEditor.Animations.AnimatorController.AllocateAnimatorClip(name); + AssetDatabase.AddObjectToAsset(clip, controller); + + // Create a state in the animatior controller for this clip + var state = controller.AddMotion(clip); + + // Add a transition property + controller.AddParameter(name, AnimatorControllerParameterType.Trigger); + + // Add an any state transition + var stateMachine = controller.layers[0].stateMachine; + var transition = stateMachine.AddAnyStateTransition(state); + transition.AddCondition(UnityEditor.Animations.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/Selectable/UXSelectableEditor.cs.meta b/Editor/UX/Selectable/UXSelectableEditor.cs.meta new file mode 100644 index 0000000..378949a --- /dev/null +++ b/Editor/UX/Selectable/UXSelectableEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1e63c9ae25da41d482430905b232d091 +timeCreated: 1765183957 \ No newline at end of file diff --git a/Editor/UX/Slider.meta b/Editor/UX/Slider.meta new file mode 100644 index 0000000..3d83e93 --- /dev/null +++ b/Editor/UX/Slider.meta @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..757079a --- /dev/null +++ b/Editor/UX/Slider/UXSliderEditor.cs @@ -0,0 +1,158 @@ +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 new file mode 100644 index 0000000..5404089 --- /dev/null +++ b/Editor/UX/Slider/UXSliderEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e67f2a4d5c0a4f399f67374dcfd42a1f +timeCreated: 1765260896 \ No newline at end of file diff --git a/Editor/UX/UXButtonEditor.cs b/Editor/UX/UXButtonEditor.cs deleted file mode 100644 index 8379b1f..0000000 --- a/Editor/UX/UXButtonEditor.cs +++ /dev/null @@ -1,732 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AlicizaX.UI.Extension; -using AlicizaX.UI.Extension.Editor; -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 : Editor -{ - private enum TabType - { - Image, - Sound, - Event - } - - GUIContent m_VisualizeNavigation = EditorGUIUtility.TrTextContent("Visualize", "Show navigation flows between selectable UI elements."); - private static bool s_ShowNavigation = false; - private static string s_ShowNavigationKey = "SelectableEditor.ShowNavigation"; - private static List s_Editors = new List(); - - private SerializedProperty m_Interactable; - private SerializedProperty m_Mode; - private SerializedProperty m_OnValueChanged; - private SerializedProperty m_OnClick; - private SerializedProperty m_UXGroup; - private SerializedProperty m_TransitionData; - private SerializedProperty m_ChildTransitions; - - private SerializedProperty m_SelectionState; - - 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 TabType currentTab = TabType.Image; - private GUISkin customSkin; - - private SerializedProperty hoverAudioClip; - private SerializedProperty clickAudioClip; - - private SerializedProperty m_Navigation; - private UXButton mTarget; - - private void OnEnable() - { - mTarget = (UXButton)target; - customSkin = AssetDatabase.LoadAssetAtPath("Packages/com.alicizax.unity.ui.extension/Editor/Res/GUISkin/UIExtensionGUISkin.guiskin"); - m_Interactable = serializedObject.FindProperty("m_Interactable"); - 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_TransitionData = serializedObject.FindProperty("m_TransitionData"); - m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions"); - m_SelectionState = serializedObject.FindProperty("m_SelectionState"); - m_Navigation = serializedObject.FindProperty("m_Navigation"); - hoverAudioClip = serializedObject.FindProperty("hoverAudioClip"); - clickAudioClip = serializedObject.FindProperty("clickAudioClip"); - - CreateChildTransitionList(); - - s_Editors.Add(this); - RegisterStaticOnSceneGUI(); - s_ShowNavigation = EditorPrefs.GetBool(s_ShowNavigationKey); - } - - protected virtual void OnDisable() - { - s_Editors.Remove(this); - RegisterStaticOnSceneGUI(); - } - - #region Navigation - - 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 - - private void CreateChildTransitionList() - { - m_ChildTransitionList = new ReorderableList(serializedObject, m_ChildTransitions, true, false, true, true); - // m_ChildTransitionList.drawHeaderCallback = (rect) => { EditorGUI.LabelField(rect, "Other Transitions"); }; - - 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); }; - } - - 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 DrawTabButton(TabType tabType, string label, string iconName) - { - bool isActive = currentTab == tabType; - var style = new GUIStyle(EditorStyles.toolbarButton) - { - fixedHeight = 25, - fontSize = 11, - fontStyle = isActive ? FontStyle.Bold : FontStyle.Normal - }; - - var content = new GUIContent( - EditorGUIUtility.IconContent(iconName).image, - label - ); - - if (GUILayout.Button(content, style)) - { - currentTab = tabType; - } - - // 高亮当前选中的标签页 - 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)); - } - } - - public override void OnInspectorGUI() - { - serializedObject.Update(); - - EditorGUILayout.BeginHorizontal(); - - // 图像标签页按钮 - DrawTabButton(TabType.Image, "Image", "d_Texture Icon"); - - // 声音标签页按钮 - DrawTabButton(TabType.Sound, "Sound", "d_AudioSource Icon"); - - // 事件标签页按钮 - DrawTabButton(TabType.Event, "Event", "EventTrigger Icon"); - - EditorGUILayout.EndHorizontal(); - - switch (currentTab) - { - case TabType.Image: - DrawGraphicsTab(); - break; - case TabType.Sound: - DrawAudioTab(); - break; - case TabType.Event: - DrawEventTab(); - break; - } - - serializedObject.ApplyModifiedProperties(); - } - - private void DrawGraphicsTab() - { - 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(); - - - EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_Mode"),new GUIContent("Navigation")); - Rect toggleRect = EditorGUILayout.GetControlRect(); - toggleRect.xMin += EditorGUIUtility.labelWidth; - s_ShowNavigation = GUI.Toggle(toggleRect, s_ShowNavigation, m_VisualizeNavigation, EditorStyles.miniButton); - - - - if (EditorGUI.EndChangeCheck()) - { - EditorPrefs.SetBool(s_ShowNavigationKey, s_ShowNavigation); - SceneView.RepaintAll(); - } - - var interactable = GUILayoutHelper.DrawToggle(m_Interactable.boolValue, customSkin, "Interactable"); - if (interactable != m_Interactable.boolValue) - { - mTarget.Interactable = interactable; - m_SelectionState.enumValueIndex = interactable ? 0 : 4; - } - - m_Interactable.boolValue = interactable; - - GUILayout.Space(1); - DrawSelfTransition(); - - GUILayout.Space(5); - DrawChildTransitions(); - GUILayout.Space(1); - - DrawBasicSettings(); - } - - private void DrawEventTab() - { - if (m_Mode.enumValueIndex == (int)ButtonModeType.Toggle) - { - EditorGUILayout.Space(); - EditorGUILayout.PropertyField(m_OnValueChanged); - } - else - { - EditorGUILayout.Space(); - EditorGUILayout.PropertyField(m_OnClick); - } - - EditorGUILayout.Separator(); - } - - private void DrawAudioTab() - { - 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 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 PlayAudio(AudioClip clip) - { - if (clip != null) - { - ExtensionHelper.PreviewAudioClip(clip); - } - } - - private void DrawChildTransitions() - { - m_ChildTransitionList.DoLayoutList(); - } - - - private float CalculateTransitionDataHeight(SerializedProperty transitionData) - { - float height = 0; - SerializedProperty transition = transitionData.FindPropertyRelative("transition"); - var currentTransition = GetTransition(transition); - - 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); - - 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); - 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); - } - } - - private void DrawSelfTransition() - { - GUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("Main Transition", EditorStyles.boldLabel); - SerializedProperty targetGraphic = m_TransitionData.FindPropertyRelative("targetGraphic"); - var graphic = targetGraphic.objectReferenceValue as Graphic; - if (graphic == null) - { - graphic = (target as UXButton).GetComponent(); - targetGraphic.objectReferenceValue = graphic; - } - - SerializedProperty transition = m_TransitionData.FindPropertyRelative("transition"); - var currentTransition = GetTransition(transition); - - switch (currentTransition) - { - case Selectable.Transition.ColorTint: - case Selectable.Transition.SpriteSwap: - EditorGUILayout.PropertyField(targetGraphic); - break; - } - - - EditorGUILayout.PropertyField(transition); - - var animator = graphic != null ? graphic.GetComponent() : (target as UXButton).GetComponent(); - switch (currentTransition) - { - case Selectable.Transition.ColorTint: - if (graphic == null) - EditorGUILayout.HelpBox("需要Graphic组件来使用颜色过渡", MessageType.Warning); - 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); - break; - } - - switch (currentTransition) - { - case Selectable.Transition.ColorTint: - CheckAndSetColorDefaults(m_TransitionData.FindPropertyRelative("colors"), targetGraphic); - EditorGUILayout.PropertyField(m_TransitionData.FindPropertyRelative("colors")); - break; - case Selectable.Transition.SpriteSwap: - EditorGUILayout.PropertyField(m_TransitionData.FindPropertyRelative("spriteState")); - break; - case Selectable.Transition.Animation: - EditorGUILayout.PropertyField(m_TransitionData.FindPropertyRelative("animationTriggers")); - if (animator == null || animator.runtimeAnimatorController == null) - { - if (GUILayout.Button("Auto Generate Animation")) - { - var animationTrigger = m_TransitionData.FindPropertyRelative("animationTriggers"); - var controller = GenerateSelectableAnimatorContoller(animationTrigger, target as UXButton); - if (controller != null) - { - if (animator == null) - animator = (target as UXButton).gameObject.AddComponent(); - - UnityEditor.Animations.AnimatorController.SetAnimatorController(animator, controller); - } - } - } - - 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--; - EditorGUILayout.Space(); - GUILayout.EndVertical(); - } - - private static UnityEditor.Animations.AnimatorController GenerateSelectableAnimatorContoller(SerializedProperty property, UXButton target) - { - if (target == null) - return null; - - 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; - - var controller = UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(path); - GenerateTriggerableTransition(normalName, controller); - GenerateTriggerableTransition(highlightedName, controller); - GenerateTriggerableTransition(pressedName, controller); - GenerateTriggerableTransition(selectedName, controller); - GenerateTriggerableTransition(disabledName, controller); - - AssetDatabase.ImportAsset(path); - - return controller; - } - - private static AnimationClip GenerateTriggerableTransition(string name, UnityEditor.Animations.AnimatorController controller) - { - // Create the clip - var clip = UnityEditor.Animations.AnimatorController.AllocateAnimatorClip(name); - AssetDatabase.AddObjectToAsset(clip, controller); - - // Create a state in the animatior controller for this clip - var state = controller.AddMotion(clip); - - // Add a transition property - controller.AddParameter(name, AnimatorControllerParameterType.Trigger); - - // Add an any state transition - var stateMachine = controller.layers[0].stateMachine; - var transition = stateMachine.AddAnyStateTransition(state); - transition.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, name); - return clip; - } - - private static string GetSaveControllerPath(UXButton 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); - } - - void CheckAndSetColorDefaults(SerializedProperty colorBlock, SerializedProperty targetGraphic) - { - bool isDirty = false; - string[] colorProps = new string[] { "m_NormalColor", "m_HighlightedColor", "m_PressedColor", "m_SelectedColor", "m_DisabledColor" }; - foreach (var propName in colorProps) - { - SerializedProperty prop = colorBlock.FindPropertyRelative(propName); - Color color = prop.colorValue; - if (color.r == 0 && color.g == 0 && color.b == 0 && color.a == 0) - { - isDirty = true; - if (prop.name == "m_PressedColor") - { - prop.colorValue = new Color(0.7843137f, 0.7843137f, 0.7843137f, 1.0f); - } - else if (prop.name == "m_DisabledColor") - { - prop.colorValue = new Color(0.7843137f, 0.7843137f, 0.7843137f, 0.5f); - } - else - { - prop.colorValue = Color.white; - } - } - } - - SerializedProperty fadeDuration = colorBlock.FindPropertyRelative("m_FadeDuration"); - SerializedProperty m_ColorMultiplier = colorBlock.FindPropertyRelative("m_ColorMultiplier"); - if (isDirty) - { - m_ColorMultiplier.floatValue = 1f; - fadeDuration.floatValue = 0.1f; - } - - var graphic = targetGraphic.objectReferenceValue as Graphic; - if (graphic != null) - { - if (!EditorApplication.isPlaying && (Selectable.Transition)m_SelectionState.enumValueIndex != Selectable.Transition.Animation) - { - Color color = colorBlock.FindPropertyRelative("m_NormalColor").colorValue; - graphic.canvasRenderer.SetColor(color); - } - } - } - - static Selectable.Transition GetTransition(SerializedProperty transition) - { - return (Selectable.Transition)transition.enumValueIndex; - } -} diff --git a/Editor/UX/UXGroupInspector.cs b/Editor/UX/UXGroupInspector.cs deleted file mode 100644 index 53d626d..0000000 --- a/Editor/UX/UXGroupInspector.cs +++ /dev/null @@ -1,50 +0,0 @@ -using AlicizaX.Editor; -using UnityEditor; - -namespace UnityEngine.UI -{ - [CustomEditor(typeof(UXGroup))] - public class UXGroupInspector : GameFrameworkInspector - { - private SerializedProperty m_Buttons; - private UXGroup _target; - - private void OnEnable() - { - _target = (UXGroup)target; - m_Buttons = serializedObject.FindProperty("m_Buttons"); - } - - public override void OnInspectorGUI() - { - serializedObject.Update(); - - GUI.enabled = false; - DrawHotButtonListAlwaysExpanded(m_Buttons); - GUI.enabled = true; - - EditorGUILayout.Space(); - - serializedObject.ApplyModifiedProperties(); - } - - private void DrawHotButtonListAlwaysExpanded(SerializedProperty listProp) - { - EditorGUILayout.LabelField("Hot Buttons", EditorStyles.boldLabel); - - if (listProp.arraySize == 0) - { - EditorGUILayout.HelpBox("当前没有绑定任何 UXButton", MessageType.Info); - return; - } - - for (int i = 0; i < listProp.arraySize; i++) - { - var element = listProp.GetArrayElementAtIndex(i); - var uxButton = element.objectReferenceValue as UXButton; - string name = uxButton != null ? uxButton.name : "Null"; - EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXButton), true); - } - } - } -} diff --git a/Runtime/UXComponent/Button/UXButton.cs b/Runtime/UXComponent/Button/UXButton.cs index abbdf35..8f2339b 100644 --- a/Runtime/UXComponent/Button/UXButton.cs +++ b/Runtime/UXComponent/Button/UXButton.cs @@ -15,35 +15,14 @@ public enum ButtonModeType Toggle } -[Serializable] -public class TransitionData -{ - public Graphic targetGraphic; - public Selectable.Transition transition = Selectable.Transition.ColorTint; - public ColorBlock colors = ColorBlock.defaultColorBlock; - public SpriteState spriteState; - public AnimationTriggers animationTriggers = new(); -} - -internal enum SelectionState -{ - Normal, - Highlighted, - Pressed, - Selected, - Disabled, -} - -[ExecuteInEditMode] +[ExecuteAlways] [DisallowMultipleComponent] public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler { #region Serialized Fields - [SerializeField] private bool m_Interactable = true; [SerializeField] private ButtonModeType m_Mode; [SerializeField] private Button.ButtonClickedEvent m_OnClick = new(); - [SerializeField] private TransitionData m_TransitionData = new(); [SerializeField] private List m_ChildTransitions = new(); [SerializeField] private UXGroup m_UXGroup; [SerializeField] private AudioClip hoverAudioClip; @@ -54,7 +33,6 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand #region Private Fields - [SerializeField] private SelectionState m_SelectionState = SelectionState.Normal; private bool m_IsDown; private bool m_HasExitedWhileDown; private bool _mTogSelected; @@ -84,17 +62,6 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand private Animator Animator => _animator ? _animator : _animator = GetComponent(); - public bool Interactable - { - get => m_Interactable; - set - { - if (m_Interactable == value) return; - m_Interactable = value; - SetState(m_Interactable ? SelectionState.Normal : SelectionState.Disabled); - } - } - public bool Selected { get => _mTogSelected; @@ -117,9 +84,7 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand _mTogSelected = value; m_OnValueChanged?.Invoke(value); - // ------------- 关键修复 ------------- // 如果当前控件处于聚焦(由导航/SetSelected 进入),那么视觉上应保持 Selected(无论逻辑是否为 selected)。 - // 聚焦判定使用 m_IsNavFocused(OnSelect/OnDeselect 管理)或 EventSystem.current.currentSelectedGameObject == gameObject。 bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject; bool isFocused = m_IsNavFocused || isEventSystemSelected; @@ -155,7 +120,8 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand protected override void Awake() { base.Awake(); - _waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_TransitionData.colors.fadeDuration)); + // 使用基类 m_MainTransition 的 fadeDuration + _waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_MainTransition.colors.fadeDuration)); ApplyVisualState(m_SelectionState, true); } @@ -166,10 +132,6 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand base.OnDestroy(); } - protected override void OnSetProperty() - { - ApplyVisualState(m_SelectionState, true); - } public override bool IsInteractable() { @@ -519,8 +481,8 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand private void ForceSetState(SelectionState state) { - m_SelectionState = state; - ApplyVisualState(state, false); + // 链接到基类的 ForceSetState + base.ForceSetState(state); } #endregion @@ -554,74 +516,33 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand } } + /// + /// 设置状态的入口(使用基类 SetState) + /// + /// private void SetState(SelectionState state) { - if (m_SelectionState == state) return; - m_SelectionState = state; - ApplyVisualState(state, false); + ForceSetState(state); } - private void ApplyVisualState(SelectionState state, bool instant) + /// + /// 覆盖基类的 ApplyVisualState:先由基类处理 MainTransition,再处理 child transitions(保持原逻辑) + /// + public override void ApplyVisualState(SelectionState state, bool instant) { - ApplyTransition(m_TransitionData, state, instant); + // main transition 由基类处理 + base.ApplyVisualState(state, instant); + + // child transitions 保持原有处理(UXButton 特有) for (int i = 0; i < m_ChildTransitions.Count; i++) - ApplyTransition(m_ChildTransitions[i], state, instant); + base.ApplyTransition(m_ChildTransitions[i], state, instant); } - private void ApplyTransition(TransitionData data, SelectionState state, bool instant) - { - if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation) - return; - - (Color color, Sprite sprite, string trigger) = state switch - { - SelectionState.Normal => (data.colors.normalColor, data.spriteState.highlightedSprite, data.animationTriggers.normalTrigger), - SelectionState.Highlighted => (data.colors.highlightedColor, data.spriteState.highlightedSprite, data.animationTriggers.highlightedTrigger), - SelectionState.Pressed => (data.colors.pressedColor, data.spriteState.pressedSprite, data.animationTriggers.pressedTrigger), - SelectionState.Selected => (data.colors.selectedColor, data.spriteState.selectedSprite, data.animationTriggers.selectedTrigger), - SelectionState.Disabled => (data.colors.disabledColor, data.spriteState.disabledSprite, data.animationTriggers.disabledTrigger), - _ => (Color.white, null, null) - }; - - 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; - } - } - - private void TweenColor(TransitionData data, Color color, bool instant) - { - if (data.targetGraphic == null) return; - if (Application.isPlaying) - { - data.targetGraphic.CrossFadeColor( - color, - instant ? 0f : data.colors.fadeDuration, - true, - true - ); - } - else - { - data.targetGraphic.canvasRenderer.SetColor(color); - } - } - - private static void SwapSprite(TransitionData data, Sprite sprite) - { - if (data.targetGraphic is Image img) - img.overrideSprite = sprite; - } - - private void PlayAnimation(string trigger) + /// + /// 覆盖 PlayAnimation:使用 UXButton 的 Animator 与 trigger 缓存(保持原逻辑) + /// + /// + protected override void PlayAnimation(string trigger) { if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger)) return; diff --git a/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs b/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs index 8d79a7d..f571a51 100644 --- a/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs +++ b/Runtime/UXComponent/Hotkey/HotkeyBindComponent.cs @@ -22,7 +22,7 @@ namespace AlicizaX.UI.Extension.UXComponent.Hotkey _holderObjectBase.OnWindowClosedEvent -= UnBindHotKeys; } [SerializeField] - private UXHotkey[] hotButtons; + private UXHotkeyButton[] hotButtons; internal void BindHotKeys() { diff --git a/Runtime/UXComponent/Hotkey/UXHotkey.cs.meta b/Runtime/UXComponent/Hotkey/UXHotkey.cs.meta deleted file mode 100644 index aa1f0fb..0000000 --- a/Runtime/UXComponent/Hotkey/UXHotkey.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 9aa1d0e145a6435c94190809bbc99235 -timeCreated: 1760340422 \ No newline at end of file diff --git a/Runtime/UXComponent/Hotkey/UXHotkey.cs b/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs similarity index 52% rename from Runtime/UXComponent/Hotkey/UXHotkey.cs rename to Runtime/UXComponent/Hotkey/UXHotkeyButton.cs index e91ecc5..22ec8d6 100644 --- a/Runtime/UXComponent/Hotkey/UXHotkey.cs +++ b/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs @@ -1,29 +1,19 @@ -#if INPUTSYSTEM_SUPPORT -using System; using UnityEngine; namespace AlicizaX.UI.Extension { [DisallowMultipleComponent] - public class UXHotkey : MonoBehaviour + public class UXHotkeyButton : UXButton { - [SerializeField] private UXButton _button; - [SerializeField] internal UnityEngine.InputSystem.InputActionReference _hotKeyRefrence; [SerializeField] internal EHotkeyPressType _hotkeyPressType; - private void OnValidate() - { - _button = GetComponent(); - } - internal void HotkeyActionTrigger() { - if (_button.Interactable) + if (Interactable) { - _button.OnSubmit(null); + OnSubmit(null); } } } } -#endif diff --git a/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs.meta b/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs.meta new file mode 100644 index 0000000..26a2ba6 --- /dev/null +++ b/Runtime/UXComponent/Hotkey/UXHotkeyButton.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42ae64be990942c899bebfffed4b48a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 337b039db051cab44819dc51e6af1f43, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs index 52d48b9..729d33c 100644 --- a/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs +++ b/Runtime/UXComponent/Hotkey/UXHotkeyRegisterManager.cs @@ -4,6 +4,7 @@ using UnityEngine.InputSystem; using System.Collections.Generic; using System.Collections; using AlicizaX.UI.Extension; + internal enum EHotkeyPressType { Started, @@ -15,9 +16,9 @@ internal static class UXHotkeyRegisterManager private readonly struct HotkeyRegistration { public readonly EHotkeyPressType pressType; - public readonly UXHotkey button; + public readonly UXHotkeyButton button; - public HotkeyRegistration(UXHotkey btn, EHotkeyPressType pressType) + public HotkeyRegistration(UXHotkeyButton btn, EHotkeyPressType pressType) { button = btn; this.pressType = pressType; @@ -32,8 +33,8 @@ internal static class UXHotkeyRegisterManager new Dictionary, InputActionReference)>(32); - private static readonly Dictionary _buttonRegistrations = - new Dictionary(64); + private static readonly Dictionary _buttonRegistrations = + new Dictionary(64); #if UNITY_EDITOR @@ -51,7 +52,7 @@ internal static class UXHotkeyRegisterManager } #endif - internal static void RegisterHotkey(UXHotkey button, InputActionReference action, EHotkeyPressType pressType) + internal static void RegisterHotkey(UXHotkeyButton button, InputActionReference action, EHotkeyPressType pressType) { if (action == null || action.action == null || button == null) return; @@ -92,7 +93,7 @@ internal static class UXHotkeyRegisterManager } } - public static void UnregisterHotkey(UXHotkey button) + public static void UnregisterHotkey(UXHotkeyButton button) { if (button == null || !_buttonRegistrations.TryGetValue(button, out var actionId)) return; @@ -150,7 +151,7 @@ internal static class UXHotkeyRegisterManager public static class UXHotkeyHotkeyExtension { - public static void BindHotKey(this UXHotkey button) + public static void BindHotKey(this UXHotkeyButton button) { if (button == null) return; @@ -165,7 +166,7 @@ public static class UXHotkeyHotkeyExtension } } - public static void UnBindHotKey(this UXHotkey button) + public static void UnBindHotKey(this UXHotkeyButton button) { if (button == null || button._hotKeyRefrence == null) return; UXHotkeyRegisterManager.UnregisterHotkey(button); diff --git a/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs b/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs new file mode 100644 index 0000000..ff79ad8 --- /dev/null +++ b/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs @@ -0,0 +1,133 @@ +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 new file mode 100644 index 0000000..83495e7 --- /dev/null +++ b/Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs.meta @@ -0,0 +1,3 @@ +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 index 2f712ed..3719979 100644 --- a/Runtime/UXComponent/Selectable/SetPropertyUtility.cs +++ b/Runtime/UXComponent/Selectable/SetPropertyUtility.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using UnityEngine; -namespace AlicizaX.UI.Extension +namespace UnityEngine.UI { internal static class SetPropertyUtility { diff --git a/Runtime/UXComponent/Selectable/UXNavigation.cs b/Runtime/UXComponent/Selectable/UXNavigation.cs index d0b9fe2..11dfd1f 100644 --- a/Runtime/UXComponent/Selectable/UXNavigation.cs +++ b/Runtime/UXComponent/Selectable/UXNavigation.cs @@ -2,7 +2,7 @@ using System; using UnityEngine; using UnityEngine.UI; -namespace AlicizaX.UI.Extension +namespace UnityEngine.UI { [Serializable] public struct UXNavigation : IEquatable diff --git a/Runtime/UXComponent/Selectable/UXSelectable.cs b/Runtime/UXComponent/Selectable/UXSelectable.cs index 10ddb3f..f20433e 100644 --- a/Runtime/UXComponent/Selectable/UXSelectable.cs +++ b/Runtime/UXComponent/Selectable/UXSelectable.cs @@ -1,22 +1,50 @@ using System; using System.Collections.Generic; -using UnityEngine; using UnityEngine.EventSystems; -namespace AlicizaX.UI.Extension +namespace UnityEngine.UI { - public class UXSelectable : - UIBehaviour, + [Serializable] + public class TransitionData + { + public Graphic targetGraphic; + 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 { - protected static UXSelectable[] s_Selectables = new UXSelectable[10]; - protected static int s_SelectableCount = 0; - protected int m_CurrentIndex = -1; - protected bool m_EnableCalled = false; + [Serializable] + public enum SelectionState + { + Normal, + Highlighted, + Pressed, + Selected, + Disabled, + } + #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; } @@ -24,14 +52,43 @@ namespace AlicizaX.UI.Extension protected bool m_GroupsAllowInteraction = true; private readonly List m_CanvasGroupCache = new List(); - [SerializeField] private UXNavigation m_Navigation = UXNavigation.defaultNavigation; + // 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; + + 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(); + if (SetPropertyUtility.SetStruct(ref m_Navigation, value)) + OnSetProperty(); + } + } + + public bool Interactable + { + get => m_Interactable; + set + { + if (m_Interactable == value) return; + m_Interactable = value; + OnSetProperty(); } } @@ -45,10 +102,14 @@ namespace AlicizaX.UI.Extension } } + #endregion - protected virtual void OnSetProperty() + #region Unity Lifecycle + + protected override void Awake() { - // override if needed + base.Awake(); + // nothing specific here; subclasses may initialize visuals } protected override void OnEnable() @@ -77,6 +138,9 @@ namespace AlicizaX.UI.Extension m_GroupsAllowInteraction = ParentGroupAllowsInteraction(); m_EnableCalled = true; + + // Ensure visual state matches current settings immediately + OnSetProperty(); } protected override void OnDisable() @@ -90,6 +154,8 @@ namespace AlicizaX.UI.Extension s_Selectables[m_CurrentIndex] = s_Selectables[s_SelectableCount]; s_Selectables[s_SelectableCount] = null; + InstantClearState(); + base.OnDisable(); m_EnableCalled = false; } @@ -104,6 +170,10 @@ namespace AlicizaX.UI.Extension } } + #endregion + + #region Groups & Interactable + protected bool ParentGroupAllowsInteraction() { Transform t = transform; @@ -126,9 +196,176 @@ namespace AlicizaX.UI.Extension public virtual bool IsInteractable() { - return m_GroupsAllowInteraction; + 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; @@ -201,12 +438,6 @@ namespace AlicizaX.UI.Extension return dir; } - void Navigate(AxisEventData eventData, UXSelectable sel) - { - if (sel != null && sel.IsActive()) - eventData.selectedObject = sel.gameObject; - } - public virtual UXSelectable FindSelectableOnLeft() { if (m_Navigation.mode == UXNavigation.Mode.Explicit) @@ -262,6 +493,16 @@ namespace AlicizaX.UI.Extension } } + 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) @@ -269,28 +510,143 @@ namespace AlicizaX.UI.Extension 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) + { + // base does nothing — subclasses (e.g. UXButton) can override to use Animator. + } + + #endregion } } diff --git a/Runtime/UXComponent/Selectable/UXSelectable.cs.meta b/Runtime/UXComponent/Selectable/UXSelectable.cs.meta index fbd51a2..7928979 100644 --- a/Runtime/UXComponent/Selectable/UXSelectable.cs.meta +++ b/Runtime/UXComponent/Selectable/UXSelectable.cs.meta @@ -1,3 +1,11 @@ fileFormatVersion: 2 guid: 0e3d476361ae4f1ea2e5143663661f3c -timeCreated: 1764667719 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 1ab39039c9d7d844aa962517519f0ad6, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/UXSlider.meta b/Runtime/UXComponent/UXSlider.meta new file mode 100644 index 0000000..104c9fd --- /dev/null +++ b/Runtime/UXComponent/UXSlider.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b940f76ec6e9b0a45a47464a6f3d9616 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/UXSlider/UXSlider.cs b/Runtime/UXComponent/UXSlider/UXSlider.cs new file mode 100644 index 0000000..9332f2f --- /dev/null +++ b/Runtime/UXComponent/UXSlider/UXSlider.cs @@ -0,0 +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() + { + } + } +} diff --git a/Runtime/UXComponent/UXSlider/UXSlider.cs.meta b/Runtime/UXComponent/UXSlider/UXSlider.cs.meta new file mode 100644 index 0000000..33e649a --- /dev/null +++ b/Runtime/UXComponent/UXSlider/UXSlider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 000a654558f849878601d3b0ed350623 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: dfe8ade815753dc4d9ca3ce5d981cb91, type: 3} + userData: + assetBundleName: + assetBundleVariant: