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 SerializedProperty m_animator; // 抽象皮肤(现在放在基类) 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"); m_animator = serializedObject.FindProperty("_animator"); // load customSkin once in base; 调整路径为你项目中 GUISkin 的实际路径(必要时修改) customSkin = AssetDatabase.LoadAssetAtPath("Packages/com.alicizax.unity.ui.extension/Editor/Res/GUISkin/UIExtensionGUISkin.guiskin"); // Ensure default Image tab exists and contains DrawSelectableInspector EnsureDefaultImageTab(); } protected virtual void OnDisable() { 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); Animator animator = null; if (target is UXSelectable anima) { animator = anima.GetComponent(); } else if (m_animator.objectReferenceValue != null) { animator = (Animator)m_animator.objectReferenceValue; } 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 } }