com.alicizax.unity.ui.exten.../Editor/UX/Selectable/UXSelectableEditor.cs

598 lines
25 KiB
C#
Raw Normal View History

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;
2025-12-15 13:42:08 +08:00
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");
2025-12-15 13:42:08 +08:00
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)
2025-12-16 16:48:44 +08:00
{
var modeProp = m_Navigation.FindPropertyRelative("m_Mode");
EditorGUILayout.PropertyField(modeProp, new GUIContent("Navigation"));
int value = modeProp.intValue;
int explicitMask = (int)UXNavigation.Mode.Explicit;
bool onlyExplicit = (value & explicitMask) == explicitMask && (value & ~explicitMask) == 0;
if (!modeProp.hasMultipleDifferentValues && 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"));
}
}
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<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);
2025-12-15 13:42:08 +08:00
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
}
}