com.alicizax.unity.ui.exten.../Editor/UX/Selectable/UXSelectableEditor.cs
2025-12-16 16:51:40 +08:00

610 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<UXSelectableEditor> s_Editors = new List<UXSelectableEditor>();
// 序列化属性(基类需要)
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<Action> callbacks = new List<Action>();
public EditorTab(string t, string icon)
{
title = t;
iconName = icon;
}
}
private List<EditorTab> _tabs = new List<EditorTab>();
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<GUISkin>("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)
/// <summary>
/// 注册一个新 tab如果已存在同名 tab则替换其 icon 和清空回调,再加入 drawCallback
/// </summary>
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);
}
/// <summary>
/// 在已存在 tab 后追加一个 callback若 tab 不存在则创建并追加。
/// </summary>
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);
}
}
}
/// <summary>
/// 从指定 tab 中移除某个 callback子类在 OnDisable 应该调用以清理)
/// </summary>
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);
}
/// <summary>
/// 卸载整个 tab移除该 title 的所有 callbacks 与 tab 本身)。
/// 如果当前选中的索引超出范围,会自动修正为合法值。
/// 注意:基类默认创建的 "Image" 页签通常不应被移除,若需要保护可以在此加判断。
/// </summary>
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
/// <summary>
/// 子类调用:绘制基类的通用 InspectorNavigation / Interactable / Main Transition
/// </summary>
protected void DrawSelectableInspector()
{
if (m_Navigation != null)
{
var modeProp = m_Navigation.FindPropertyRelative("m_Mode");
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = modeProp.hasMultipleDifferentValues;
UXNavigation.Mode cur = (UXNavigation.Mode)modeProp.intValue;
UXNavigation.Mode next = (UXNavigation.Mode)EditorGUILayout.EnumFlagsField("Navigation", cur);
EditorGUI.showMixedValue = false;
if (EditorGUI.EndChangeCheck())
{
modeProp.intValue = (int)next;
}
int explicitMask = (int)UXNavigation.Mode.Explicit;
int value = modeProp.intValue;
bool onlyExplicit = !modeProp.hasMultipleDifferentValues && (value & explicitMask) == explicitMask && (value & ~explicitMask) == 0;
if (onlyExplicit)
{
EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_SelectOnUp"));
EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_SelectOnDown"));
EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_SelectOnLeft"));
EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_SelectOnRight"));
}
serializedObject.ApplyModifiedProperties();
}
DrawToggleShowNavigation();
if (customSkin != null)
{
var interactable = GUILayoutHelper.DrawToggle(m_Interactable.boolValue, customSkin, "Interactable");
if (interactable != m_Interactable.boolValue)
{
(target as UXSelectable).Interactable = interactable;
m_Interactable.boolValue = interactable;
var selProp = serializedObject.FindProperty("m_SelectionState");
if (selProp != null)
{
selProp.enumValueIndex = interactable ? (int)UXSelectable.SelectionState.Normal : (int)UXSelectable.SelectionState.Disabled;
}
}
}
GUILayout.Space(1);
serializedObject.ApplyModifiedProperties();
DrawSelfTransition();
GUILayout.Space(5);
}
#region MainTransition helpers (moved to base)
protected virtual void DrawSelfTransition()
{
if (m_MainTransition == null)
return;
GUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Main Transition", EditorStyles.boldLabel);
SerializedProperty targetGraphic = m_MainTransition.FindPropertyRelative("targetGraphic");
var graphic = targetGraphic.objectReferenceValue as Graphic;
if (graphic == null)
{
graphic = (target as UXSelectable).GetComponent<Graphic>();
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<Animator>();
}
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<Animator>();
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
}
}