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; } }