大优化

大优化 懒得写了 重构了分离了 UXButton UXToggle UXSelectable UXGroup
This commit is contained in:
陈思海 2025-12-19 20:26:22 +08:00
parent 64c8c338c6
commit 551423f09d
44 changed files with 2618 additions and 3428 deletions

View File

@ -3,7 +3,7 @@ using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace AlicizaX.UI.Extension
namespace UnityEditor.Extensions
{
internal static class ExtensionHelper
{

View File

@ -2,7 +2,7 @@ using System;
using UnityEditor;
using UnityEngine;
namespace AlicizaX.UI.Extension
namespace UnityEditor.DrawUtils
{
internal class GUILayoutHelper
{
@ -39,7 +39,7 @@ namespace AlicizaX.UI.Extension
}
public static void DrawProperty<T>(SerializedProperty property, GUISkin skin, string content,
Action<T, T> changeCallBack, T defaultValue = default(T))
Action<T, T> changeCallBack=null, T defaultValue = default(T))
{
GUILayout.BeginHorizontal(EditorStyles.helpBox);
EditorGUILayout.LabelField(new GUIContent(content), skin.FindStyle("Text"), GUILayout.Width(120));

View File

@ -0,0 +1,170 @@
// TabbedInspector.cs
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace UnityEditor.Utils
{
/// <summary>
/// 复用的 tab 管理器(非 Editor供任意 Editor 组合使用。
/// 用法:在 Editor 的 OnEnable 创建一个实例并 RegisterTab / AppendToTab
/// 在 OnInspectorGUI 调用 DrawTabs()。
/// </summary>
public class TabbedInspector
{
public class Tab
{
public string title;
public string iconName;
public List<Action> callbacks = new List<Action>();
public Tab(string t, string icon)
{
title = t;
iconName = icon;
}
}
List<Tab> _tabs = new List<Tab>();
int _currentTabIndex = 0;
string _prefsKey; // 用于保存每类 inspector 的选择(可选)
public TabbedInspector(string prefsKey = null)
{
_prefsKey = prefsKey;
if (!string.IsNullOrEmpty(_prefsKey))
{
_currentTabIndex = EditorPrefs.GetInt(_prefsKey, 0);
}
}
public 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 Tab(title, iconName);
_tabs.Add(tab);
}
else
{
tab.iconName = iconName;
tab.callbacks.Clear();
}
if (drawCallback != null)
tab.callbacks.Add(drawCallback);
}
public 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 Tab(title, "d_DefaultAsset Icon");
_tabs.Add(tab);
}
if (!tab.callbacks.Contains(drawCallback))
{
if (last) tab.callbacks.Add(drawCallback);
else tab.callbacks.Insert(0, drawCallback);
}
}
public 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);
}
public 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);
}
public void EnsureDefaultTab(string title, string iconName, Action defaultCallback)
{
var tab = _tabs.Find(t => t.title == title);
if (tab == null)
{
tab = new Tab(title, iconName);
_tabs.Insert(0, tab);
}
if (!tab.callbacks.Contains(defaultCallback))
tab.callbacks.Insert(0, defaultCallback);
}
public void DrawTabs()
{
if (_tabs == null || _tabs.Count == 0)
{
// nothing to draw
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 icon = EditorGUIUtility.IconContent(tab.iconName)?.image;
var content = new GUIContent(icon, tab.title);
if (GUILayout.Button(content, style))
{
_currentTabIndex = i;
SaveIndex();
}
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 callbacks for current tab
var callbacks = _tabs[_currentTabIndex].callbacks;
if (callbacks != null)
{
foreach (var cb in callbacks)
{
try
{
cb?.Invoke();
}
catch (ExitGUIException)
{
throw;
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
}
void SaveIndex()
{
if (!string.IsNullOrEmpty(_prefsKey))
EditorPrefs.SetInt(_prefsKey, _currentTabIndex);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2033905cec9b4aaeadee445667ca163b
timeCreated: 1766127466

View File

@ -41,19 +41,17 @@ public class UXCreateHelper : Editor
}
[MenuItem("GameObject/UI/UXSlider")]
public static void CreateUXSlider(MenuCommand menuCommand)
[MenuItem("GameObject/UI/UXToggle")]
public static void CreateUXToggle(MenuCommand menuCommand)
{
Type MenuOptionsType = typeof(UnityEditor.UI.SliderEditor).Assembly.GetType("UnityEditor.UI.MenuOptions");
InvokeMethod(MenuOptionsType, "AddSlider", new object[] { menuCommand });
InvokeMethod(MenuOptionsType, "AddToggle", new object[] { menuCommand });
GameObject obj = Selection.activeGameObject;
obj.name = "UXSlider";
DestroyImmediate(obj.GetComponent<Slider>());
var uxSlider = obj.AddComponent<UXSlider>();
uxSlider.fillRect = obj.transform.Find("Fill Area/Fill").GetComponent<RectTransform>();
var handle = obj.transform.Find("Handle Slide Area/Handle").GetComponent<RectTransform>();
uxSlider.handleRect = handle;
uxSlider.targetGraphic = handle.GetComponent<Graphic>();
obj.name = "UXToggle";
DestroyImmediate(obj.GetComponent<Toggle>());
var uxToggle = obj.AddComponent<UXToggle>();
uxToggle.graphic = obj.transform.Find("Background/Checkmark").GetComponent<Graphic>();
uxToggle.targetGraphic = obj.transform.Find("Background").GetComponent<Graphic>();
}
#if TEXTMESHPRO_SUPPORT

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

View File

@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: ead857c5c78826747a7ab33355cd8108
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:

View File

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Text;
using UnityEditor.AnimatedValues;
using UnityEditor.Animations;
using UnityEditor.DrawUtils;
using UnityEditor.Extensions;
using UnityEditor.Utils;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.UI;
using AnimatorController = UnityEditor.Animations.AnimatorController;
using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType;
namespace UnityEditor.UI
{
[CustomEditor(typeof(UXButton), true)]
[CanEditMultipleObjects]
internal class UButtonEditor : UXSelectableEditor
{
SerializedProperty m_OnClickProperty;
#if INPUTSYSTEM_SUPPORT
private SerializedProperty _hotKeyRefrence;
private SerializedProperty _hotkeyPressType;
#endif
private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip;
protected override void OnEnable()
{
base.OnEnable();
m_OnClickProperty = serializedObject.FindProperty("m_OnClick");
#if INPUTSYSTEM_SUPPORT
_hotKeyRefrence = serializedObject.FindProperty("_hotkeyAction");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
#endif
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
clickAudioClip = serializedObject.FindProperty("clickAudioClip");
_tabs.RegisterTab("Sound", "d_AudioSource Icon", DrawSoundTab);
_tabs.RegisterTab("Event", "EventTrigger Icon", DrawEventTab);
}
protected override void OnDisable()
{
base.OnDisable();
_tabs.UnregisterTab("Sound");
_tabs.UnregisterTab("Event");
}
private void DrawEventTab()
{
EditorGUILayout.Space();
serializedObject.Update();
EditorGUILayout.PropertyField(m_OnClickProperty);
#if INPUTSYSTEM_SUPPORT
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField("Hotkey Setting", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_hotKeyRefrence, new GUIContent("InputAction"));
EditorGUILayout.PropertyField(_hotkeyPressType, new GUIContent("PressType"));
}
#endif
serializedObject.ApplyModifiedProperties();
}
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);
}
});
}
private void PlayAudio(AudioClip clip)
{
if (clip != null)
{
ExtensionHelper.PreviewAudioClip(clip);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2d771091d826488b9ff5bc2799c11756
timeCreated: 1766125678

View File

@ -1,401 +0,0 @@
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<UXGroup>(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<Animator>() : 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<Animator>() : 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--;
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 1a6fb65845fb481293f57b30b1bfcb3b
timeCreated: 1744275051

View File

@ -0,0 +1,270 @@
using System.Collections.Generic;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
namespace UnityEditor.UI
{
[CustomEditor(typeof(UXGroup))]
public class UXGroupInspector : Editor
{
private SerializedProperty m_Toggles;
private SerializedProperty m_AllowSwitchOff;
private SerializedProperty m_DefaultToggle;
private UXGroup _target;
private ReorderableList _reorderableList;
private void OnEnable()
{
_target = (UXGroup)target;
m_Toggles = serializedObject.FindProperty("m_Toggles");
m_AllowSwitchOff = serializedObject.FindProperty("m_AllowSwitchOff");
m_DefaultToggle = serializedObject.FindProperty("m_DefaultToggle");
_reorderableList = new ReorderableList(serializedObject, m_Toggles, true, true, true, true)
{
drawHeaderCallback = DrawHeader,
drawElementCallback = DrawElement,
onRemoveCallback = OnRemoveList,
onChangedCallback = OnChanged,
displayAdd = false,
};
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(m_AllowSwitchOff);
// Default selector: only show toggles that are currently in the group's m_Toggles list
DrawDefaultToggleSelector();
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();
// 在编辑器下尽量实时同步状态
if (!Application.isPlaying)
{
// 保证序列化数据写入后同步 group 状态
_target.EnsureValidState();
}
}
private void DrawDefaultToggleSelector()
{
// Build a list of current toggles (non-null)
List<UXToggle> toggles = new List<UXToggle>();
for (int i = 0; i < m_Toggles.arraySize; i++)
{
var elem = m_Toggles.GetArrayElementAtIndex(i);
var t = elem.objectReferenceValue as UXToggle;
if (t != null)
toggles.Add(t);
}
// Prepare options array with a "None" entry
string[] options = new string[toggles.Count + 1];
options[0] = "<None>";
for (int i = 0; i < toggles.Count; i++)
{
options[i + 1] = string.Format("[{0}] {1}", i, toggles[i] != null ? toggles[i].name : "Null");
}
// Determine current index
UXToggle currentDefault = m_DefaultToggle.objectReferenceValue as UXToggle;
int currentIndex = 0;
if (currentDefault != null)
{
int found = toggles.IndexOf(currentDefault);
if (found >= 0)
currentIndex = found + 1; // +1 because 0 is <None>
else
{
// Current default is not in the list -> clear it
m_DefaultToggle.objectReferenceValue = null;
serializedObject.ApplyModifiedProperties();
currentIndex = 0;
}
}
EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup("Default Toggle", currentIndex, options);
if (EditorGUI.EndChangeCheck())
{
UXToggle newDefault = null;
if (newIndex > 0)
newDefault = toggles[newIndex - 1];
m_DefaultToggle.objectReferenceValue = newDefault;
serializedObject.ApplyModifiedProperties();
// 如果选择了一个非空默认项,则在 allowSwitchOff == false 时将其设为选中状态(并通知组)
if (newDefault != null)
{
// 确保该 toggle 在 group 中(理论上应该如此)
if (!_target.ContainsToggle(newDefault))
{
_target.RegisterToggle(newDefault);
}
if (!_target.allowSwitchOff)
{
// 通过 SerializedObject 修改 UXToggle 的 m_IsOn避免触发不必要的回调
SerializedObject so = new SerializedObject(newDefault);
var isOnProp = so.FindProperty("m_IsOn");
isOnProp.boolValue = true;
so.ApplyModifiedProperties();
// 通知组同步其余项
_target.NotifyToggleOn(newDefault);
}
}
else
{
// 选择 None不自动切换状态。但如果组不允许 all-off则会在 EnsureValidState 中被处理
}
}
}
private void DrawHeader(Rect rect)
{
EditorGUI.LabelField(rect, "Toggles", EditorStyles.boldLabel);
}
// 记录旧的引用用于侦测变化
private UXToggle previousRef;
private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
SerializedProperty element = m_Toggles.GetArrayElementAtIndex(index);
rect.y += 2;
Rect fieldRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight);
UXToggle oldButton = element.objectReferenceValue as UXToggle;
string label = $"[{index}] {(oldButton != null ? oldButton.name : "Null")}";
EditorGUI.BeginChangeCheck();
var newRef = EditorGUI.ObjectField(fieldRect, label, oldButton, typeof(UXToggle), true) as UXToggle;
if (EditorGUI.EndChangeCheck())
{
// 先处理 Remove旧值存在且不同
if (oldButton != null && oldButton != newRef)
{
OnRemove(oldButton);
}
// 再处理 Add新值非空
if (newRef != null && oldButton != newRef)
{
OnAdd(newRef);
}
// 最后把引用写回去
element.objectReferenceValue = newRef;
serializedObject.ApplyModifiedProperties();
}
}
private void OnAddList(ReorderableList list)
{
int newIndex = m_Toggles.arraySize;
m_Toggles.arraySize++;
serializedObject.ApplyModifiedProperties();
var newElem = m_Toggles.GetArrayElementAtIndex(newIndex);
newElem.objectReferenceValue = null;
serializedObject.ApplyModifiedProperties();
}
private void OnRemoveList(ReorderableList list)
{
if (list.index < 0 || list.index >= m_Toggles.arraySize)
return;
var oldButton = m_Toggles.GetArrayElementAtIndex(list.index).objectReferenceValue as UXToggle;
if (oldButton)
{
OnRemove(oldButton);
}
m_Toggles.DeleteArrayElementAtIndex(list.index);
serializedObject.ApplyModifiedProperties();
}
private void OnChanged(ReorderableList list)
{
serializedObject.ApplyModifiedProperties();
// 编辑器变动后同步 group 状态和默认项
if (!Application.isPlaying)
_target.EnsureValidState();
}
// ========================
// 自动调用的新增方法
// ========================
private void OnAdd(UXToggle toggle)
{
if (toggle == null)
return;
SerializedObject so = new SerializedObject(toggle);
var groupProp = so.FindProperty("m_Group");
groupProp.objectReferenceValue = target;
so.ApplyModifiedProperties();
UXGroup group = (UXGroup)target;
group.RegisterToggle(toggle);
// 添加后尽量同步组状态(处理默认项或冲突)
if (!Application.isPlaying)
group.EnsureValidState();
}
private void OnRemove(UXToggle toggle)
{
if (toggle == null)
return;
SerializedObject so = new SerializedObject(toggle);
var groupProp = so.FindProperty("m_Group");
UXGroup group = groupProp.objectReferenceValue as UXGroup;
if (group != null)
group.UnregisterToggle(toggle);
groupProp.objectReferenceValue = null;
so.ApplyModifiedProperties();
// 如果移除的正好是默认项,则清空默认选项
if (m_DefaultToggle != null && m_DefaultToggle.objectReferenceValue == toggle)
{
m_DefaultToggle.objectReferenceValue = null;
serializedObject.ApplyModifiedProperties();
}
if (!Application.isPlaying && _target != null)
_target.EnsureValidState();
}
}
}

View File

@ -1,153 +0,0 @@
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();
}
}
}

View File

@ -1,13 +1,11 @@
#if INPUTSYSTEM_SUPPORT
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using AlicizaX.Editor;
using AlicizaX.UI;
using AlicizaX.UI.Runtime;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
namespace AlicizaX.UI111
namespace UnityEditor.UI
{
[CustomEditor(typeof(HotkeyBindComponent))]
public class HotkeyBindComponentInspector : UnityEditor.Editor
@ -25,7 +23,6 @@ namespace AlicizaX.UI111
{
serializedObject.Update();
var holder = _target.GetComponent<UIHolderObjectBase>();
if (holder == null)
{
@ -37,14 +34,14 @@ namespace AlicizaX.UI111
EditorGUILayout.Space();
// 🔸 灰掉显示 hotButtons 列表(不可手动编辑)
// 灰掉显示 hotButtons 列表(不可手动编辑)
GUI.enabled = false;
DrawHotButtonListAlwaysExpanded(hotButtonsProp);
GUI.enabled = true;
EditorGUILayout.Space();
// 🔘 按钮:扫描子物体(包含隐藏)
// 按钮:扫描子物体(包含隐藏)
if (GUILayout.Button("🔍 扫描所有子物体 (包含隐藏对象)"))
{
FindAllUXHotkeys();
@ -60,7 +57,7 @@ namespace AlicizaX.UI111
{
EditorGUILayout.LabelField("Hot Buttons", EditorStyles.boldLabel);
if (listProp.arraySize == 0)
if (listProp == null || listProp.arraySize == 0)
{
EditorGUILayout.HelpBox("当前没有绑定任何 UXHotkey。", MessageType.Info);
return;
@ -70,11 +67,11 @@ namespace AlicizaX.UI111
for (int i = 0; i < listProp.arraySize; i++)
{
var element = listProp.GetArrayElementAtIndex(i);
var uxHotkey = element.objectReferenceValue as UXHotkeyButton;
string name = uxHotkey != null ? uxHotkey.name : "Null";
EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXHotkeyButton), true);
var comp = element.objectReferenceValue as Component;
string name = comp != null ? comp.name + " (" + comp.GetType().Name + ")" : "Null";
// 注意:这里用 typeof(Component)
EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(Component), true);
}
EditorGUI.indentLevel--;
}
@ -85,28 +82,18 @@ namespace AlicizaX.UI111
{
Undo.RecordObject(_target, "Scan UXHotkey");
var uxHotkeys = _target.GetComponentsInChildren<UXHotkeyButton>(true);
List<UXHotkeyButton> valiedHotkeys = new List<UXHotkeyButton>();
foreach (var item in uxHotkeys)
{
var field = item.GetType()
.GetField("_hotKeyRefrence", BindingFlags.NonPublic | BindingFlags.Instance);
var hotRefrenceObject = field.GetValue(item);
if (hotRefrenceObject != null) valiedHotkeys.Add(item);
}
_target.GetType()
.GetField("hotButtons", BindingFlags.NonPublic | BindingFlags.Instance)
?.SetValue(_target, valiedHotkeys.ToArray());
var collectMethod = target.GetType().GetMethod("CollectUXHotkeys", BindingFlags.NonPublic | BindingFlags.Instance);
collectMethod.Invoke(target, null);
EditorUtility.SetDirty(_target);
serializedObject.Update();
Debug.Log($"[HotkeyBindComponent] 已找到 {uxHotkeys.Length} 个 UXHotkey 组件并绑定。");
if (collectMethod != null)
{
collectMethod.Invoke(target, null);
EditorUtility.SetDirty(_target);
serializedObject.Update();
}
else
{
Debug.LogWarning("未找到 CollectUXHotkeys 方法。");
}
}
}
}
#endif

View File

@ -1,34 +0,0 @@
#if INPUTSYSTEM_SUPPORT
using AlicizaX.UI;
using UnityEditor;
using UnityEngine;
namespace AlicizaX.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"));
}
}
}
}
#endif

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: dc0b010e4f884f6992de90c5e5dd7154
timeCreated: 1765185454

View File

@ -1,410 +1,344 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AlicizaX.UI.Extension;
using UnityEditor;
using UnityEditor.SceneManagement;
using System.Text;
using UnityEditor.AnimatedValues;
using UnityEditor.Animations;
using UnityEditor.DrawUtils;
using UnityEditor.Utils;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.UI;
using AnimatorController = UnityEditor.Animations.AnimatorController;
using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType;
namespace UnityEngine.UI
namespace UnityEditor.UI
{
[CanEditMultipleObjects]
[CustomEditor(typeof(UXSelectable), true)]
internal class UXSelectableEditor : Editor
internal class UXSelectableEditor : SelectableEditor
{
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>();
private SerializedProperty m_ChildTransitions;
private ReorderableList m_ChildTransitionList;
public TabbedInspector _tabs;
// 序列化属性(基类需要)
protected SerializedProperty m_Navigation;
protected SerializedProperty m_MainTransition;
protected SerializedProperty m_Interactable;
protected SerializedProperty m_animator;
SerializedProperty m_ChildInteractableProperty;
SerializedProperty m_ChildTargetGraphicProperty;
SerializedProperty m_ChildTransitionProperty;
SerializedProperty m_ChildColorBlockProperty;
SerializedProperty m_ChildSpriteStateProperty;
SerializedProperty m_ChildAnimTriggerProperty;
SerializedProperty m_ChildNavigationProperty;
AnimBool m_ChildShowColorTint = new AnimBool();
AnimBool m_ChildShowSpriteTrasition = new AnimBool();
AnimBool m_ChildShowAnimTransition = new AnimBool();
// 抽象皮肤(现在放在基类)
protected GUISkin customSkin;
// Tab 管理结构
protected class EditorTab
GUIContent m_ChildVisualizeNavigation = EditorGUIUtility.TrTextContent("Visualize", "Show navigation flows between selectable UI elements.");
private static bool s_ChildShowNavigation = false;
private static string s_ChildShowNavigationKey = "SelectableEditor.ShowNavigation";
private static Color darkZebraEven = new Color(0.22f, 0.22f, 0.22f);
private static Color darkZebraOdd = new Color(0.27f, 0.27f, 0.27f);
protected override void OnEnable()
{
public string title;
public string iconName;
public List<Action> callbacks = new List<Action>();
base.OnEnable();
public EditorTab(string t, string icon)
{
title = t;
iconName = icon;
}
}
m_ChildInteractableProperty = serializedObject.FindProperty("m_Interactable");
m_ChildTargetGraphicProperty = serializedObject.FindProperty("m_TargetGraphic");
m_ChildTransitionProperty = serializedObject.FindProperty("m_Transition");
m_ChildColorBlockProperty = serializedObject.FindProperty("m_Colors");
m_ChildSpriteStateProperty = serializedObject.FindProperty("m_SpriteState");
m_ChildAnimTriggerProperty = serializedObject.FindProperty("m_AnimationTriggers");
m_ChildNavigationProperty = serializedObject.FindProperty("m_Navigation");
m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions");
private List<EditorTab> _tabs = new List<EditorTab>();
private int _currentTabIndex = 0;
CreateChildTransitionList();
protected virtual void OnEnable()
{
s_ShowNavigation = EditorPrefs.GetBool(s_ShowNavigationKey);
s_Editors.Add(this);
RegisterStaticOnSceneGUI();
m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions");
var trans = GetTransition(m_ChildTransitionProperty);
m_ChildShowColorTint.value = (trans == Selectable.Transition.ColorTint);
m_ChildShowSpriteTrasition.value = (trans == Selectable.Transition.SpriteSwap);
m_ChildShowAnimTransition.value = (trans == Selectable.Transition.Animation);
m_ChildShowColorTint.valueChanged.AddListener(Repaint);
m_ChildShowSpriteTrasition.valueChanged.AddListener(Repaint);
_tabs = new TabbedInspector(typeof(UXSelectable).FullName + ".TabbedIndex");
_tabs.EnsureDefaultTab("Image", "d_Texture Icon", DrawBaseButtonInspector);
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()
protected override void OnDisable()
{
s_Editors.Remove(this);
RegisterStaticOnSceneGUI();
_tabs.Clear();
_currentTabIndex = 0;
base.OnDisable();
m_ChildShowColorTint.valueChanged.RemoveListener(Repaint);
m_ChildShowSpriteTrasition.valueChanged.RemoveListener(Repaint);
_tabs.UnregisterTab("Image");
}
#region Tab API (Register / Append / Remove / Unregister)
/// <summary>
/// 注册一个新 tab如果已存在同名 tab则替换其 icon 和清空回调,再加入 drawCallback
/// </summary>
protected void RegisterTab(string title, string iconName, Action drawCallback)
public override void OnInspectorGUI()
{
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);
serializedObject.Update();
_tabs.DrawTabs();
serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// 在已存在 tab 后追加一个 callback若 tab 不存在则创建并追加。
/// </summary>
protected void AppendToTab(string title, Action drawCallback, bool last = true)
void DrawBaseButtonInspector()
{
if (string.IsNullOrEmpty(title) || drawCallback == null) return;
var tab = _tabs.Find(t => t.title == title);
if (tab == null)
serializedObject.Update();
var interactable = GUILayoutHelper.DrawToggle(m_ChildInteractableProperty.boolValue, customSkin, "Interactable");
if (interactable != m_ChildInteractableProperty.boolValue)
{
tab = new EditorTab(title, "d_DefaultAsset Icon");
_tabs.Add(tab);
m_ChildInteractableProperty.boolValue = interactable;
}
// 避免重复追加(简单判断)
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);
}
EditorGUILayout.PropertyField(m_ChildNavigationProperty);
/// <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()
{
EditorGUI.BeginChangeCheck();
Rect toggleRect = EditorGUILayout.GetControlRect();
toggleRect.xMin += EditorGUIUtility.labelWidth;
EditorGUI.BeginChangeCheck();
s_ShowNavigation = GUI.Toggle(toggleRect, s_ShowNavigation, m_VisualizeNavigation, EditorStyles.miniButton);
s_ChildShowNavigation = GUI.Toggle(toggleRect, s_ChildShowNavigation, m_ChildVisualizeNavigation, EditorStyles.miniButton);
if (EditorGUI.EndChangeCheck())
{
EditorPrefs.SetBool(s_ShowNavigationKey, s_ShowNavigation);
EditorPrefs.SetBool(s_ChildShowNavigationKey, s_ChildShowNavigation);
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;
var trans = GetTransition(m_ChildTransitionProperty);
var graphic = m_ChildTargetGraphicProperty.objectReferenceValue as Graphic;
if (graphic == null)
graphic = (target as Selectable).GetComponent<Graphic>();
var animator = (target as Selectable).GetComponent<Animator>();
m_ChildShowColorTint.target = (!m_ChildTransitionProperty.hasMultipleDifferentValues && trans == Button.Transition.ColorTint);
m_ChildShowSpriteTrasition.target = (!m_ChildTransitionProperty.hasMultipleDifferentValues && trans == Button.Transition.SpriteSwap);
m_ChildShowAnimTransition.target = (!m_ChildTransitionProperty.hasMultipleDifferentValues && trans == Button.Transition.Animation);
EditorGUILayout.PropertyField(m_ChildTransitionProperty);
{
graphic = (target as UXSelectable).GetComponent<Graphic>();
if (targetGraphic != null)
targetGraphic.objectReferenceValue = graphic;
if (trans == Selectable.Transition.ColorTint || trans == Selectable.Transition.SpriteSwap)
{
EditorGUILayout.PropertyField(m_ChildTargetGraphicProperty);
}
switch (trans)
{
case Selectable.Transition.ColorTint:
if (graphic == null)
EditorGUILayout.HelpBox("You must have a Graphic target in order to use a color transition.", MessageType.Warning);
break;
case Selectable.Transition.SpriteSwap:
if (graphic as Image == null)
EditorGUILayout.HelpBox("You must have a Image target in order to use a sprite swap transition.", MessageType.Warning);
break;
}
if (EditorGUILayout.BeginFadeGroup(m_ChildShowColorTint.faded))
{
EditorGUILayout.PropertyField(m_ChildColorBlockProperty);
}
EditorGUILayout.EndFadeGroup();
if (EditorGUILayout.BeginFadeGroup(m_ChildShowSpriteTrasition.faded))
{
EditorGUILayout.PropertyField(m_ChildSpriteStateProperty);
}
EditorGUILayout.EndFadeGroup();
if (EditorGUILayout.BeginFadeGroup(m_ChildShowAnimTransition.faded))
{
EditorGUILayout.PropertyField(m_ChildAnimTriggerProperty);
if (animator == null || animator.runtimeAnimatorController == null)
{
Rect buttonRect = EditorGUILayout.GetControlRect();
buttonRect.xMin += EditorGUIUtility.labelWidth;
if (GUI.Button(buttonRect, "Auto Generate Animation", EditorStyles.miniButton))
{
var controller = GenerateSelectableAnimatorContoller((target as Selectable).animationTriggers, target as Selectable);
if (controller != null)
{
if (animator == null)
animator = (target as Selectable).gameObject.AddComponent<Animator>();
Animations.AnimatorController.SetAnimatorController(animator, controller);
}
}
}
}
EditorGUILayout.EndFadeGroup();
}
SerializedProperty transition = m_MainTransition.FindPropertyRelative("transition");
EditorGUILayout.Space();
GUILayout.EndVertical();
GUILayout.Space(5);
m_ChildTransitionList.DoLayoutList();
GUILayout.Space(1);
serializedObject.ApplyModifiedProperties();
}
#region ChildTransition
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);
};
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 DrawTransitionData(Rect position, SerializedProperty transitionData)
{
SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic");
SerializedProperty transition = transitionData.FindPropertyRelative("transition");
SerializedProperty colorBlock = transitionData.FindPropertyRelative("colors");
SerializedProperty spriteState = transitionData.FindPropertyRelative("spriteState");
EditorGUI.indentLevel++;
float lineHeight = EditorGUIUtility.singleLineHeight;
float spacing = 2f;
float y = position.y;
var currentTransition = GetTransition(transition);
if (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:
EditorGUILayout.PropertyField(targetGraphic);
Rect targetRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.PropertyField(targetRect, targetGraphic);
y += lineHeight + spacing;
break;
}
EditorGUILayout.PropertyField(transition);
Rect transitionRect = new Rect(position.x, y, position.width, lineHeight);
Animator animator = null;
if (target is UXSelectable anima)
{
animator = anima.GetComponent<Animator>();
}
else if (m_animator.objectReferenceValue != null)
{
animator = (Animator)m_animator.objectReferenceValue;
}
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;
var graphic = targetGraphic.objectReferenceValue as Graphic;
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
if (graphic == null)
EditorGUILayout.HelpBox("需要Graphic组件来使用颜色过渡", MessageType.Warning);
{
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))
EditorGUILayout.HelpBox("需要Image组件来使用精灵切换", MessageType.Warning);
break;
case Selectable.Transition.Animation:
if (animator == null)
EditorGUILayout.HelpBox("需要Animator组件来使用动画切换", MessageType.Warning);
{
Rect warningRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.HelpBox(warningRect, "需要Image组件来使用精灵切换", MessageType.Warning);
y += lineHeight + spacing;
}
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>();
CheckAndSetColorDefaults(colorBlock, targetGraphic);
Rect colorRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(colorBlock));
UnityEditor.Animations.AnimatorController.SetAnimatorController(animator, controller);
}
}
if (EditorGUILayout.BeginFadeGroup(m_ChildShowColorTint.faded))
{
EditorGUI.PropertyField(colorRect, colorBlock);
}
EditorGUILayout.EndFadeGroup();
break;
case Selectable.Transition.SpriteSwap:
CheckAndSetColorDefaults(colorBlock, targetGraphic);
Rect spriteRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(spriteState));
if (EditorGUILayout.BeginFadeGroup(m_ChildShowSpriteTrasition.faded))
{
EditorGUI.PropertyField(spriteRect, spriteState);
}
EditorGUILayout.EndFadeGroup();
break;
}
@ -415,8 +349,51 @@ namespace UnityEngine.UI
graphic.canvasRenderer.SetColor(Color.white);
}
EditorGUILayout.Space();
GUILayout.EndVertical();
EditorGUI.indentLevel--;
}
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;
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;
}
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("colors"));
break;
case Selectable.Transition.SpriteSwap:
height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("spriteState"));
break;
}
return height;
}
protected void CheckAndSetColorDefaults(SerializedProperty colorBlock, SerializedProperty targetGraphic)
@ -471,28 +448,34 @@ namespace UnityEngine.UI
}
}
protected static UnityEditor.Animations.AnimatorController GenerateSelectableAnimatorContoller(SerializedProperty property, UnityEngine.Object targetObj)
#endregion
#region Static Method
static Selectable.Transition GetTransition(SerializedProperty transition)
{
if (targetObj == null || property == null)
return (Selectable.Transition)transition.enumValueIndex;
}
private static AnimatorController GenerateSelectableAnimatorContoller(AnimationTriggers animationTriggers, Selectable target)
{
if (target == null)
return null;
var targetAsGO = (targetObj as UXSelectable);
var path = GetSaveControllerPath(targetAsGO);
// Where should we create the controller?
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;
// figure out clip names
var normalName = string.IsNullOrEmpty(animationTriggers.normalTrigger) ? "Normal" : animationTriggers.normalTrigger;
var highlightedName = string.IsNullOrEmpty(animationTriggers.highlightedTrigger) ? "Highlighted" : animationTriggers.highlightedTrigger;
var pressedName = string.IsNullOrEmpty(animationTriggers.pressedTrigger) ? "Pressed" : animationTriggers.pressedTrigger;
var selectedName = string.IsNullOrEmpty(animationTriggers.selectedTrigger) ? "Selected" : animationTriggers.selectedTrigger;
var disabledName = string.IsNullOrEmpty(animationTriggers.disabledTrigger) ? "Disabled" : animationTriggers.disabledTrigger;
var controller = UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(path);
// Create controller and hook up transitions.
var controller = AnimatorController.CreateAnimatorControllerAtPath(path);
GenerateTriggerableTransition(normalName, controller);
GenerateTriggerableTransition(highlightedName, controller);
GenerateTriggerableTransition(pressedName, controller);
@ -504,10 +487,65 @@ namespace UnityEngine.UI
return controller;
}
private static AnimationClip GenerateTriggerableTransition(string name, UnityEditor.Animations.AnimatorController controller)
private static string GetSaveControllerPath(Selectable 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);
}
private static void SetUpCurves(AnimationClip highlightedClip, AnimationClip pressedClip, string animationPath)
{
string[] channels = { "m_LocalScale.x", "m_LocalScale.y", "m_LocalScale.z" };
var highlightedKeys = new[] { new Keyframe(0f, 1f), new Keyframe(0.5f, 1.1f), new Keyframe(1f, 1f) };
var highlightedCurve = new AnimationCurve(highlightedKeys);
foreach (var channel in channels)
AnimationUtility.SetEditorCurve(highlightedClip, EditorCurveBinding.FloatCurve(animationPath, typeof(Transform), channel), highlightedCurve);
var pressedKeys = new[] { new Keyframe(0f, 1.15f) };
var pressedCurve = new AnimationCurve(pressedKeys);
foreach (var channel in channels)
AnimationUtility.SetEditorCurve(pressedClip, EditorCurveBinding.FloatCurve(animationPath, typeof(Transform), channel), pressedCurve);
}
private static string BuildAnimationPath(Selectable target)
{
// if no target don't hook up any curves.
var highlight = target.targetGraphic;
if (highlight == null)
return string.Empty;
var startGo = highlight.gameObject;
var toFindGo = target.gameObject;
var pathComponents = new Stack<string>();
while (toFindGo != startGo)
{
pathComponents.Push(startGo.name);
// didn't exist in hierarchy!
if (startGo.transform.parent == null)
return string.Empty;
startGo = startGo.transform.parent.gameObject;
}
// calculate path
var animPath = new StringBuilder();
if (pathComponents.Count > 0)
animPath.Append(pathComponents.Pop());
while (pathComponents.Count > 0)
animPath.Append("/").Append(pathComponents.Pop());
return animPath.ToString();
}
private static AnimationClip GenerateTriggerableTransition(string name, AnimatorController controller)
{
// Create the clip
var clip = UnityEditor.Animations.AnimatorController.AllocateAnimatorClip(name);
var clip = AnimatorController.AllocateAnimatorClip(name);
AssetDatabase.AddObjectToAsset(clip, controller);
// Create a state in the animatior controller for this clip
@ -519,91 +557,10 @@ namespace UnityEngine.UI
// Add an any state transition
var stateMachine = controller.layers[0].stateMachine;
var transition = stateMachine.AddAnyStateTransition(state);
transition.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, name);
transition.AddCondition(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
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 708a130e1d5f49f18619c6ad9c3baccb
timeCreated: 1765260889

View File

@ -1,158 +0,0 @@
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();
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: e67f2a4d5c0a4f399f67374dcfd42a1f
timeCreated: 1765260896

3
Editor/UX/Toggle.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c51b9957a2b441edbcf2d1f264545312
timeCreated: 1766136377

View File

@ -0,0 +1,178 @@
using UnityEditor.DrawUtils;
using UnityEditor.Extensions;
using UnityEditor.SceneManagement;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
namespace UnityEditor.UI
{
[CustomEditor(typeof(UXToggle), true)]
[CanEditMultipleObjects]
internal class UXToggleEditor : UXSelectableEditor
{
SerializedProperty m_OnValueChangedProperty;
SerializedProperty m_TransitionProperty;
SerializedProperty m_GraphicProperty;
SerializedProperty m_GroupProperty;
SerializedProperty m_IsOnProperty;
#if INPUTSYSTEM_SUPPORT
private SerializedProperty _hotKeyRefrence;
private SerializedProperty _hotkeyPressType;
#endif
private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip;
protected override void OnEnable()
{
base.OnEnable();
m_TransitionProperty = serializedObject.FindProperty("toggleTransition");
m_GraphicProperty = serializedObject.FindProperty("graphic");
m_GroupProperty = serializedObject.FindProperty("m_Group");
m_IsOnProperty = serializedObject.FindProperty("m_IsOn");
m_OnValueChangedProperty = serializedObject.FindProperty("onValueChanged");
#if INPUTSYSTEM_SUPPORT
_hotKeyRefrence = serializedObject.FindProperty("_hotkeyAction");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
#endif
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
clickAudioClip = serializedObject.FindProperty("clickAudioClip");
_tabs.AppendToTab("Image", DrawImageTab);
_tabs.RegisterTab("Sound", "d_AudioSource Icon", DrawSoundTab);
_tabs.RegisterTab("Event", "EventTrigger Icon", DrawEventTab);
}
protected override void OnDisable()
{
base.OnDisable();
_tabs.UnregisterTab("Sound");
_tabs.UnregisterTab("Event");
}
private void DrawEventTab()
{
EditorGUILayout.Space();
serializedObject.Update();
EditorGUILayout.PropertyField(m_OnValueChangedProperty);
#if INPUTSYSTEM_SUPPORT
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField("Hotkey Setting", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_hotKeyRefrence, new GUIContent("InputAction"));
EditorGUILayout.PropertyField(_hotkeyPressType, new GUIContent("PressType"));
}
#endif
serializedObject.ApplyModifiedProperties();
}
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);
}
});
}
private void PlayAudio(AudioClip clip)
{
if (clip != null)
{
ExtensionHelper.PreviewAudioClip(clip);
}
}
private void DrawImageTab()
{
EditorGUILayout.Space();
serializedObject.Update();
UXToggle toggle = serializedObject.targetObject as UXToggle;
EditorGUI.BeginChangeCheck();
GUILayoutHelper.DrawProperty(m_IsOnProperty, customSkin, "Is On");
if (EditorGUI.EndChangeCheck())
{
if (!Application.isPlaying)
EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene);
UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup;
bool newIsOn = m_IsOnProperty.boolValue;
bool oldIsOn = toggle.isOn;
// 编辑器下:如果属于某组且不允许 all-off且当前正是被选中的项则禁止通过 Inspector 将其关闭
if (!Application.isPlaying && group != null && !group.allowSwitchOff && oldIsOn && !newIsOn)
{
Debug.LogWarning($"Cannot turn off toggle '{toggle.name}' because its group '{group.name}' does not allow all toggles to be off.", toggle);
// 恢复 Inspector 中的显示为 true
m_IsOnProperty.boolValue = true;
serializedObject.ApplyModifiedProperties();
}
else
{
// 使用属性赋值以保证视觉刷新PlayEffect 会在 setter 被调用)
toggle.isOn = newIsOn;
if (group != null && group.isActiveAndEnabled && toggle.IsActive())
{
if (toggle.isOn || (!group.AnyTogglesOn() && !group.allowSwitchOff))
{
toggle.isOn = true;
group.NotifyToggleOn(toggle);
}
}
}
}
GUILayoutHelper.DrawProperty(m_TransitionProperty, customSkin, "Transition");
GUILayoutHelper.DrawProperty(m_GraphicProperty, customSkin, "Graphic");
EditorGUI.BeginChangeCheck();
GUILayoutHelper.DrawProperty<UXGroup>(m_GroupProperty, customSkin, "UXGroup", (oldValue, newValue) =>
{
UXToggle self = target as UXToggle;
if (oldValue != null)
{
oldValue.UnregisterToggle(self);
}
if (newValue != null)
{
newValue.RegisterToggle(self);
}
});
if (EditorGUI.EndChangeCheck())
{
if (!Application.isPlaying)
EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene);
UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup;
// Use the property setter to ensure consistent registration/unregistration
toggle.group = group;
}
EditorGUILayout.Space();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9301ee465f2c46d08b2fade637710625
timeCreated: 1766136386

View File

@ -1,583 +1,126 @@
using System;
using System.Collections;
using System.Collections.Generic;
using AlicizaX.UI;
using AlicizaX.UI.Extension;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
#if INPUTSYSTEM_SUPPORT
using UnityEngine.InputSystem;
#endif
[Serializable]
public enum ButtonModeType
namespace UnityEngine.UI
{
Normal,
Toggle
}
[ExecuteAlways]
[DisallowMultipleComponent]
public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler
{
#region Serialized Fields
[SerializeField] private ButtonModeType m_Mode;
[SerializeField] private Button.ButtonClickedEvent m_OnClick = new();
[SerializeField] private List<TransitionData> m_ChildTransitions = new();
[SerializeField] private UXGroup m_UXGroup;
[SerializeField] private AudioClip hoverAudioClip;
[SerializeField] private AudioClip clickAudioClip;
[SerializeField] private UnityEvent<bool> m_OnValueChanged = new();
#endregion
#region Private Fields
private bool m_IsDown;
private bool m_HasExitedWhileDown;
private bool _mTogSelected;
private Coroutine _resetRoutine;
private Coroutine _deferredDeselectRoutine;
private WaitForSeconds _waitFadeDuration;
// 静态锁(用于 normal 模式点击后的“保持 Selected”并能转移
private static UXButton s_LockedButton = null;
private bool m_IsFocusLocked = false;
private bool m_IsNavFocused = false;
// ===== 新增:延迟撤销选择的 pending 标记,用于避免在协程尚未完成时读取过时的锁状态 =====
private bool _deferredDeselectPending = false;
#endregion
#region Properties
public bool Selected
[AddComponentMenu("UI/UXButton", 30)]
public class UXButton : UXSelectable, IPointerClickHandler, ISubmitHandler, IButton
#if INPUTSYSTEM_SUPPORT
, IHotkeyTrigger
#endif
{
get => _mTogSelected;
set
#if INPUTSYSTEM_SUPPORT
InputActionReference IHotkeyTrigger.HotkeyAction
{
if ((m_Mode == ButtonModeType.Normal && value) || m_Mode == ButtonModeType.Toggle)
get => _hotkeyAction;
set => _hotkeyAction = value;
}
EHotkeyPressType IHotkeyTrigger.HotkeyPressType
{
get => _hotkeyPressType;
set => _hotkeyPressType = value;
}
void IHotkeyTrigger.HotkeyActionTrigger()
{
if (interactable)
{
if (m_Mode == ButtonModeType.Toggle) _mTogSelected = !value;
HandleClick();
OnSubmit(null);
}
}
}
internal bool InternalTogSelected
{
get => _mTogSelected;
set
[SerializeField] internal InputActionReference _hotkeyAction;
[SerializeField] internal EHotkeyPressType _hotkeyPressType;
public InputActionReference HotKeyRefrence
{
if (_mTogSelected == value) return;
_mTogSelected = value;
m_OnValueChanged?.Invoke(value);
// 如果当前控件处于聚焦(由导航/SetSelected 进入),那么视觉上应保持 Selected无论逻辑是否为 selected
bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject;
bool isFocused = m_IsNavFocused || isEventSystemSelected;
if (m_Mode == ButtonModeType.Toggle && isFocused)
{
// 即使逻辑值为 false也保持 Selected 视觉,因为焦点还在该控件上
SetState(SelectionState.Selected);
}
else
{
// 否则按逻辑恢复 Selected / Normal
SetState(value ? SelectionState.Selected : SelectionState.Normal);
}
get { return _hotkeyAction; }
}
}
public Button.ButtonClickedEvent onClick
{
get => m_OnClick;
set => m_OnClick = value;
}
public UnityEvent<bool> onValueChanged
{
get => m_OnValueChanged;
set => m_OnValueChanged = value;
}
#endregion
#region Unity Lifecycle
protected override void Awake()
{
base.Awake();
// 使用基类 m_MainTransition 的 fadeDuration
_waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_MainTransition.colors.fadeDuration));
ApplyVisualState(m_SelectionState, true);
}
protected override void OnDestroy()
{
if (_resetRoutine != null)
StopCoroutine(_resetRoutine);
if (_deferredDeselectRoutine != null)
StopCoroutine(_deferredDeselectRoutine);
base.OnDestroy();
}
#endif
[SerializeField] private AudioClip hoverAudioClip;
[SerializeField] private AudioClip clickAudioClip;
public override bool IsInteractable()
{
return base.IsInteractable() && m_Interactable;
}
#endregion
#region Static lock helper
private static void SetLockedButton(UXButton newLocked)
{
if (s_LockedButton == newLocked)
return;
if (s_LockedButton != null)
protected UXButton()
{
var old = s_LockedButton;
s_LockedButton = null;
old.m_IsFocusLocked = false;
if (old._mTogSelected && old.m_Mode == ButtonModeType.Toggle)
old.SetState(SelectionState.Selected);
}
if (newLocked != null)
public override void OnPointerEnter(PointerEventData eventData)
{
s_LockedButton = newLocked;
s_LockedButton.m_IsFocusLocked = true;
s_LockedButton.SetState(SelectionState.Selected);
}
}
#endregion
#region Pointer Handlers
public override void OnPointerDown(PointerEventData eventData)
{
if (!CanProcess()) return;
m_IsDown = true;
m_HasExitedWhileDown = false;
SetState(SelectionState.Pressed);
}
public override void OnPointerUp(PointerEventData eventData)
{
if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left)
return;
m_IsDown = false;
if (m_IsFocusLocked || (_mTogSelected && m_Mode == ButtonModeType.Toggle && navigation.mode != UXNavigation.Mode.None))
{
SetState(SelectionState.Selected);
return;
base.OnPointerEnter(eventData);
PlayAudio(hoverAudioClip);
}
var newState = m_HasExitedWhileDown ? SelectionState.Normal : SelectionState.Highlighted;
SetState(newState);
}
public override void OnPointerEnter(PointerEventData eventData)
{
if (!CanProcessEnter()) return;
m_HasExitedWhileDown = false;
// 如果 toggle 模式并且聚焦(导航/或 EventSystem 选中),保持 Selected不触发 Highlight
bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject;
bool isFocused = m_IsNavFocused || isEventSystemSelected;
if (m_Mode == ButtonModeType.Toggle && isFocused)
public virtual void OnPointerClick(PointerEventData eventData)
{
SetState(SelectionState.Selected);
return;
}
if (navigation.mode != UXNavigation.Mode.None)
{
// ===== 修改:当有 deferred deselect pending 时,不应认为 focus 仍旧被 lock =====
bool focusLockedEffective = m_IsFocusLocked && !_deferredDeselectPending;
if ((m_Mode == ButtonModeType.Normal && focusLockedEffective) ||
(m_Mode == ButtonModeType.Toggle && _mTogSelected))
{
SetState(SelectionState.Selected);
if (eventData.button != PointerEventData.InputButton.Left)
return;
}
Press();
PlayAudio(clickAudioClip);
}
if (m_IsDown) return;
SetState(SelectionState.Highlighted);
PlayAudio(hoverAudioClip);
}
public override void OnPointerExit(PointerEventData eventData)
{
if (!m_Interactable) return;
if (m_IsDown)
public virtual void OnSubmit(BaseEventData eventData)
{
m_HasExitedWhileDown = true;
return;
}
// 聚焦时保持 Selected不回退到 Normal
bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject;
bool isFocused = m_IsNavFocused || isEventSystemSelected;
if (m_Mode == ButtonModeType.Toggle && isFocused)
{
SetState(SelectionState.Selected);
return;
}
if (navigation.mode != UXNavigation.Mode.None)
{
// ===== 修改:同上,不要在 deferred pending 时误用旧的 lock 状态 =====
bool focusLockedEffective = m_IsFocusLocked && !_deferredDeselectPending;
if ((m_Mode == ButtonModeType.Normal && focusLockedEffective) ||
(m_Mode == ButtonModeType.Toggle && _mTogSelected))
{
SetState(SelectionState.Selected);
Press();
PlayAudio(clickAudioClip);
// if we get set disabled during the press
// don't run the coroutine.
if (!IsActive() || !IsInteractable())
return;
}
DoStateTransition(SelectionState.Pressed, false);
StartCoroutine(OnFinishSubmit());
}
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
}
public void OnPointerClick(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left || !m_Interactable)
return;
PlayAudio(clickAudioClip);
if (m_Mode == ButtonModeType.Normal)
private IEnumerator OnFinishSubmit()
{
if (navigation.mode != UXNavigation.Mode.None)
var fadeTime = colors.fadeDuration;
var elapsedTime = 0f;
while (elapsedTime < fadeTime)
{
if (EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(gameObject, eventData);
UISystemProfilerApi.AddMarker("Button.onClick", this);
m_OnClick?.Invoke();
if (IsStillSelected())
SetLockedButton(this);
elapsedTime += Time.unscaledDeltaTime;
yield return null;
}
else
{
UISystemProfilerApi.AddMarker("Button.onClick", this);
m_OnClick?.Invoke();
}
}
else
{
HandleClick();
if (navigation.mode != UXNavigation.Mode.None)
{
if (EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(gameObject, eventData);
if (IsStillSelected())
SetLockedButton(this);
}
}
}
#endregion
#region Submit handling (Keyboard Submit)
public void OnSubmit(BaseEventData eventData)
{
if (_resetRoutine != null)
{
StopCoroutine(_resetRoutine);
_resetRoutine = null;
DoStateTransition(currentSelectionState, false);
}
if (Animator)
private void PlayAudio(AudioClip clip)
{
foreach (int id in _animTriggerCache.Values)
Animator.ResetTrigger(id);
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
}
if (EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(gameObject, eventData);
[SerializeField] private Button.ButtonClickedEvent m_OnClick = new Button.ButtonClickedEvent();
m_IsDown = true;
m_HasExitedWhileDown = false;
ForceSetState(SelectionState.Pressed);
PlayAudio(clickAudioClip);
if (m_Mode == ButtonModeType.Toggle)
public Button.ButtonClickedEvent onClick
{
_resetRoutine = StartCoroutine(SubmitToggleDeferredRoutine());
return;
get { return m_OnClick; }
set { m_OnClick = value; }
}
// ① 先执行回调(可能切走焦点)
HandleClick();
// ② 只在“焦点仍在自己”时才锁
if (navigation.mode != UXNavigation.Mode.None && IsStillSelected())
SetLockedButton(this);
if (navigation.mode != UXNavigation.Mode.None && m_Mode == ButtonModeType.Normal)
_resetRoutine = StartCoroutine(SubmitAndLockRoutine());
else
_resetRoutine = StartCoroutine(ResetAfterSubmit());
}
#endregion
#region Selection / Navigation handling
public override void OnSelect(BaseEventData eventData)
{
if (m_Navigation.mode == UXNavigation.Mode.None) return;
base.OnSelect(eventData);
m_IsNavFocused = true;
if (s_LockedButton != this)
private void Press()
{
SetLockedButton(this);
return;
}
if (!IsActive() || !IsInteractable())
return;
// 如果聚焦时无论逻辑是否选中Toggle 显示 Selected
if (m_Mode == ButtonModeType.Toggle)
{
SetState(SelectionState.Selected);
return;
}
if (m_IsDown)
{
SetState(SelectionState.Pressed);
}
else
{
if (m_IsFocusLocked)
SetState(SelectionState.Selected);
else
SetState(SelectionState.Highlighted);
}
}
private IEnumerator DeferredDeselectCheck()
{
// 标记开始:表示我们在等待 EventSystem 正确更新选择(避免竞态)
_deferredDeselectPending = true;
yield return null; // 等一帧让 EventSystem 更新 currentSelectedGameObject
bool selectionIsNull = EventSystem.current == null || EventSystem.current.currentSelectedGameObject == null;
if (selectionIsNull)
{
m_IsNavFocused = false;
// ===== 更安全的解锁:仅当静态锁指向自己时才清理它,避免影响其他按钮 =====
if (s_LockedButton == this)
s_LockedButton = null;
m_IsFocusLocked = false;
}
// 取消 pending 标记并清理协程引用
_deferredDeselectPending = false;
_deferredDeselectRoutine = null;
}
public override void OnDeselect(BaseEventData eventData)
{
base.OnDeselect(eventData);
m_IsNavFocused = false;
// 停掉上一次的延迟检查(若有)
if (_deferredDeselectRoutine != null)
{
StopCoroutine(_deferredDeselectRoutine);
_deferredDeselectRoutine = null;
_deferredDeselectPending = false;
}
// 延迟一帧再判断 EventSystem.current.currentSelectedGameObject避免读取到旧值
_deferredDeselectRoutine = StartCoroutine(DeferredDeselectCheck());
// 视觉状态先按原逻辑处理(立即更新视觉),协程会确保锁状态在正确时刻被清理
if (m_Mode == ButtonModeType.Toggle)
{
if (_mTogSelected)
SetState(SelectionState.Selected);
else
SetState(SelectionState.Normal);
return;
}
if (m_Mode == ButtonModeType.Normal && _mTogSelected)
{
SetState(SelectionState.Selected);
return;
}
SetState(SelectionState.Normal);
}
#endregion
#region Coroutines for submit/reset
private IEnumerator SubmitAndLockRoutine()
{
yield return _waitFadeDuration;
m_IsDown = false;
m_HasExitedWhileDown = false;
if (Animator)
{
foreach (int id in _animTriggerCache.Values)
Animator.ResetTrigger(id);
}
if (IsStillSelected())
{
SetLockedButton(this);
ApplyVisualState(SelectionState.Selected, false);
}
_resetRoutine = null;
}
private IEnumerator SubmitToggleDeferredRoutine()
{
yield return null;
yield return _waitFadeDuration;
HandleClick();
if (Animator)
{
foreach (int id in _animTriggerCache.Values)
Animator.ResetTrigger(id);
}
m_IsDown = false;
m_HasExitedWhileDown = false;
bool stillFocused = IsStillSelected();
if (stillFocused)
SetState(SelectionState.Selected);
else
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
if (navigation.mode != UXNavigation.Mode.None && stillFocused)
SetLockedButton(this);
_resetRoutine = null;
}
private IEnumerator ResetAfterSubmit()
{
yield return _waitFadeDuration;
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
_resetRoutine = null;
}
#endregion
#region Utility
private bool IsStillSelected()
{
if (EventSystem.current != null)
return EventSystem.current.currentSelectedGameObject == gameObject;
return m_IsNavFocused;
}
#endregion
#region Logic
private bool CanProcess()
{
return m_Interactable;
}
private bool CanProcessEnter()
{
return m_Interactable;
}
private void HandleClick()
{
if (m_Mode == ButtonModeType.Normal)
{
UISystemProfilerApi.AddMarker("Button.onClick", this);
m_OnClick?.Invoke();
}
else if (m_UXGroup)
{
m_UXGroup.NotifyButtonClicked(this);
}
else
{
InternalTogSelected = !_mTogSelected;
}
}
/// <summary>
/// 设置状态的入口(使用基类 SetState
/// </summary>
/// <param name="state"></param>
private void SetState(SelectionState state)
{
ForceSetState(state);
ApplyVisualState(state, true);
}
public override void ApplyVisualState(SelectionState state, bool instant)
{
base.ApplyVisualState(state, instant);
for (int i = 0; i < m_ChildTransitions.Count; i++)
base.ApplyTransition(m_ChildTransitions[i], state, instant);
}
private void PlayAudio(AudioClip clip)
{
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
}
#endregion
protected override void OnSetProperty()
{
base.OnSetProperty();
ApplyVisualState(m_SelectionState, true);
}
public void Focus()
{
if (gameObject != null && EventSystem.current != null)
{
EventSystem.current.SetSelectedGameObject(gameObject);
m_OnClick.Invoke();
}
}
}

View File

@ -1,143 +1,286 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System.Linq;
using UnityEngine.EventSystems;
using UnityEngine;
namespace UnityEngine.UI
{
[AddComponentMenu("UI/UXGroup", 31)]
[DisallowMultipleComponent]
[ExecuteAlways]
public class UXGroup : UIBehaviour
{
[SerializeField] private bool m_AllowSwitchOff;
[SerializeField] private List<UXButton> m_Buttons = new();
private UXButton _current;
public UnityEvent<UXButton> onSelectedChanged = new();
[SerializeField] private bool m_AllowSwitchOff = false;
public bool allowSwitchOff
{
get => m_AllowSwitchOff;
set
get { return m_AllowSwitchOff; }
set { m_AllowSwitchOff = value; }
}
[SerializeField]
private List<UXToggle> m_Toggles = new List<UXToggle>();
// 新增:默认选中的 Toggle可在 Inspector 设置)
[SerializeField]
private UXToggle m_DefaultToggle;
public UXToggle defaultToggle
{
get { return m_DefaultToggle; }
set { m_DefaultToggle = value; EnsureValidState(); }
}
protected UXGroup()
{
}
protected override void Start()
{
EnsureValidState();
base.Start();
}
protected override void OnEnable()
{
EnsureValidState();
base.OnEnable();
}
private void ValidateToggleIsInGroup(UXToggle toggle)
{
if (toggle == null || !m_Toggles.Contains(toggle))
throw new ArgumentException(string.Format("UXToggle {0} is not part of ToggleGroup {1}", new object[] { toggle, this }));
}
public void NotifyToggleOn(UXToggle toggle, bool sendCallback = true)
{
ValidateToggleIsInGroup(toggle);
for (var i = 0; i < m_Toggles.Count; i++)
{
m_AllowSwitchOff = value;
ValidateGroup();
if (m_Toggles[i] == toggle)
continue;
if (sendCallback)
m_Toggles[i].isOn = false;
else
m_Toggles[i].SetIsOnWithoutNotify(false);
}
}
protected override void Start() => ValidateGroup();
protected override void OnDestroy()
public void UnregisterToggle(UXToggle toggle)
{
foreach (var btn in m_Buttons)
if (btn)
btn.InternalTogSelected = false;
m_Buttons.Clear();
base.OnDestroy();
if (toggle == null)
return;
if (m_Toggles.Contains(toggle))
m_Toggles.Remove(toggle);
// 如果被移除的正好是默认选项,则清空默认项
if (m_DefaultToggle == toggle)
{
m_DefaultToggle = null;
}
}
public void RegisterButton(UXButton button)
public void RegisterToggle(UXToggle toggle)
{
if (!button) return;
if (m_Buttons.Contains(button)) return;
m_Buttons.Add(button);
if (toggle == null)
return;
if (button.InternalTogSelected)
if (!m_Toggles.Contains(toggle))
m_Toggles.Add(toggle);
// 当组内已有其他开启项,并且不允许 all-off 时,
// 如果新加入的 toggle 本身为 on则需要把它关闭以维持单选。
if (!allowSwitchOff)
{
if (_current && _current != button)
_current.InternalTogSelected = false;
_current = button;
// 如果已经有一个 active 的 toggle且不是刚加入的这个并且刚加入的 toggle isOn 为 true则关闭刚加入的 toggle
var firstActive = GetFirstActiveToggle();
if (firstActive != null && firstActive != toggle && toggle.isOn)
{
// 我们使用不触发回调的方式避免编辑时产生不必要的事件调用
toggle.SetIsOnWithoutNotify(false);
}
else if (firstActive == null)
{
// 没有任何 active 且不允许 all-off如果 group 指定了 defaultToggle优先选中它但仅当 default 在本组内且可交互)
if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle))
{
var dt = m_DefaultToggle;
if (dt != null && dt != toggle)
{
// 确保默认项被选中editor/runtime 都适用)
dt.isOn = true;
NotifyToggleOn(dt);
}
else if (dt == toggle)
{
// 新加入项就是默认项,确保它为 on
toggle.isOn = true;
NotifyToggleOn(toggle);
}
}
}
}
}
// 新增:判断组里是否包含某 toggle用于运行时/编辑器避免重复注册)
public bool ContainsToggle(UXToggle toggle)
{
return m_Toggles != null && m_Toggles.Contains(toggle);
}
// Ensure list consistency: clean nulls, and make sure every toggle in this list actually references this group.
public void EnsureValidState()
{
if (m_Toggles == null)
m_Toggles = new List<UXToggle>();
// Remove null references first
m_Toggles.RemoveAll(x => x == null);
// 如果不允许 all-off优先尝试选中 defaultToggle否则选中第一个。
if (!allowSwitchOff && !AnyTogglesOn() && m_Toggles.Count != 0)
{
UXToggle toSelect = null;
if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle))
{
toSelect = m_DefaultToggle;
}
else
{
toSelect = m_Toggles[0];
}
if (toSelect != null)
{
toSelect.isOn = true;
NotifyToggleOn(toSelect);
}
}
ValidateGroup();
}
IEnumerable<UXToggle> activeToggles = ActiveToggles();
public void UnregisterButton(UXButton button)
{
if (!button) return;
m_Buttons.Remove(button);
if (_current == button)
_current = null;
button.InternalTogSelected = false;
}
internal void NotifyButtonClicked(UXButton button)
{
if (!button) return;
if (button.InternalTogSelected)
if (activeToggles.Count() > 1)
{
if (m_AllowSwitchOff) SetSelected(null);
// 如果 defaultToggle 是开启的,优先保留它
UXToggle firstActive = GetFirstActiveToggle();
foreach (UXToggle toggle in activeToggles)
{
if (toggle == firstActive)
{
continue;
}
toggle.isOn = false;
}
}
// Synchronize each toggle's group reference to this group if necessary,
// but avoid causing re-ordering in cases where it's already consistent.
for (int i = 0; i < m_Toggles.Count; i++)
{
var t = m_Toggles[i];
if (t == null)
continue;
if (t.group != this)
{
// 使用 setter 会触发必要的注册/注销逻辑
t.group = this;
}
}
}
public bool AnyTogglesOn()
{
return m_Toggles.Find(x => x != null && x.isOn) != null;
}
public IEnumerable<UXToggle> ActiveToggles()
{
return m_Toggles.Where(x => x != null && x.isOn);
}
public UXToggle GetFirstActiveToggle()
{
// 优先返回 defaultToggle如果它处于 on 且在组内)
if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle) && m_DefaultToggle.isOn)
return m_DefaultToggle;
IEnumerable<UXToggle> activeToggles = ActiveToggles();
return activeToggles.Count() > 0 ? activeToggles.First() : null;
}
public void SetAllTogglesOff(bool sendCallback = true)
{
bool oldAllowSwitchOff = m_AllowSwitchOff;
m_AllowSwitchOff = true;
if (sendCallback)
{
for (var i = 0; i < m_Toggles.Count; i++)
if (m_Toggles[i] != null)
m_Toggles[i].isOn = false;
}
else
{
SetSelected(button);
for (var i = 0; i < m_Toggles.Count; i++)
if (m_Toggles[i] != null)
m_Toggles[i].SetIsOnWithoutNotify(false);
}
m_AllowSwitchOff = oldAllowSwitchOff;
}
private void SetSelected(UXButton target)
public void Next()
{
if (_current == target) return;
var previous = _current;
_current = null;
SelectAdjacent(true);
}
foreach (var btn in m_Buttons)
public void Preview()
{
SelectAdjacent(false);
}
private void SelectAdjacent(bool forward)
{
if (m_Toggles == null || m_Toggles.Count == 0)
return;
UXToggle current = GetFirstActiveToggle();
int currentIndex = current != null ? m_Toggles.IndexOf(current) : -1;
int idx = currentIndex;
if (idx == -1 && !forward)
idx = 0;
for (int step = 0; step < m_Toggles.Count; step++)
{
bool select = (btn == target);
if (btn.InternalTogSelected != select)
btn.InternalTogSelected = select;
if (select) _current = btn;
if (forward)
{
idx = (idx + 1) % m_Toggles.Count;
}
else
{
idx = (idx - 1 + m_Toggles.Count) % m_Toggles.Count;
}
UXToggle t = m_Toggles[idx];
if (t == null)
continue;
if (!t.IsActive())
continue;
if (!t.IsInteractable())
continue;
t.isOn = true;
return;
}
if (_current) _current.Focus();
if (previous != _current)
onSelectedChanged?.Invoke(_current);
}
private void ValidateGroup()
{
if (_current != null && _current.InternalTogSelected) return;
if (!m_AllowSwitchOff && m_Buttons.Count > 0)
SetSelected(m_Buttons[0]);
}
public bool AnyOtherSelected(UXButton exclude)
{
foreach (var btn in m_Buttons)
if (btn != exclude && btn.InternalTogSelected)
return true;
return false;
}
public void SelectNext() => SelectRelative(1);
public void SelectPrevious() => SelectRelative(-1);
private void SelectRelative(int dir)
{
if (m_Buttons.Count == 0) return;
int start = _current ? m_Buttons.IndexOf(_current) : -1;
int next = FindNextSelectable(start, dir);
if (next >= 0) SetSelected(m_Buttons[next]);
}
private int FindNextSelectable(int startIndex, int dir)
{
if (m_Buttons.Count == 0) return -1;
int count = m_Buttons.Count;
int index = (startIndex + dir + count) % count;
for (int i = 0; i < count; i++)
{
var btn = m_Buttons[index];
if (btn && btn.isActiveAndEnabled && btn.Interactable)
return index;
index = (index + dir + count) % count;
}
return -1;
}
}
}

View File

@ -0,0 +1,260 @@
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
#if INPUTSYSTEM_SUPPORT
using UnityEngine.InputSystem;
#endif
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEngine.UI
{
[AddComponentMenu("UI/UXToggle", 30)]
[RequireComponent(typeof(RectTransform))]
public class UXToggle : UXSelectable, IPointerClickHandler, ISubmitHandler, ICanvasElement
#if INPUTSYSTEM_SUPPORT
, IHotkeyTrigger
#endif
{
[Serializable]
public class ToggleEvent : UnityEvent<bool> { }
public Toggle.ToggleTransition toggleTransition = Toggle.ToggleTransition.Fade;
public Graphic graphic;
[SerializeField] private UXGroup m_Group;
public UXGroup group
{
get { return m_Group; }
set
{
SetToggleGroup(value, true);
PlayEffect(true);
}
}
public ToggleEvent onValueChanged = new ToggleEvent();
[Tooltip("Is the toggle currently on or off?")]
[SerializeField] private bool m_IsOn;
protected UXToggle() { }
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying)
CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
}
#endif
public virtual void Rebuild(CanvasUpdate executing)
{
#if UNITY_EDITOR
if (executing == CanvasUpdate.Prelayout)
onValueChanged.Invoke(m_IsOn);
#endif
}
public virtual void LayoutComplete() { }
public virtual void GraphicUpdateComplete() { }
protected override void OnDestroy()
{
if (m_Group != null)
m_Group.EnsureValidState();
base.OnDestroy();
}
protected override void OnEnable()
{
base.OnEnable();
PlayEffect(true);
}
protected override void OnDidApplyAnimationProperties()
{
if (graphic != null)
{
bool oldValue = !Mathf.Approximately(graphic.canvasRenderer.GetColor().a, 0);
if (m_IsOn != oldValue)
{
m_IsOn = oldValue;
Set(!oldValue);
}
}
base.OnDidApplyAnimationProperties();
}
// Centralized group setter logic.
private void SetToggleGroup(UXGroup newGroup, bool setMemberValue)
{
// 如果组没有改变,仍然需要确保组里包含此 toggle修复编辑器中批量拖拽只注册最后一项的问题
if (m_Group == newGroup)
{
if (setMemberValue)
m_Group = newGroup;
if (newGroup != null && !newGroup.ContainsToggle(this))
{
newGroup.RegisterToggle(this);
}
// 尝试同步组状态,确保编辑器批量赋值时能稳定显示
if (newGroup != null)
newGroup.EnsureValidState();
return;
}
// 从旧组注销(如果存在)
if (m_Group != null)
m_Group.UnregisterToggle(this);
if (setMemberValue)
m_Group = newGroup;
// 注册到新组(不再强依赖 IsActive(),以保证编辑器批量赋值时也能正确注册)
if (newGroup != null)
{
if (!newGroup.ContainsToggle(this))
{
newGroup.RegisterToggle(this);
}
// 如果正在 on通知组维持单选逻辑
if (isOn)
newGroup.NotifyToggleOn(this);
// 同步组的内部状态,确保 Inspector 列表正确显示
newGroup.EnsureValidState();
}
}
public bool isOn
{
get { return m_IsOn; }
set { Set(value); }
}
public void SetIsOnWithoutNotify(bool value)
{
Set(value, false);
}
void Set(bool value, bool sendCallback = true)
{
if (m_IsOn == value)
return;
m_IsOn = value;
if (m_Group != null && m_Group.isActiveAndEnabled && IsActive())
{
if (m_IsOn || (!m_Group.AnyTogglesOn() && !m_Group.allowSwitchOff))
{
m_IsOn = true;
m_Group.NotifyToggleOn(this, sendCallback);
}
}
PlayEffect(toggleTransition == Toggle.ToggleTransition.None);
if (sendCallback)
{
UISystemProfilerApi.AddMarker("Toggle.value", this);
onValueChanged.Invoke(m_IsOn);
}
}
private void PlayEffect(bool instant)
{
if (graphic == null)
return;
#if UNITY_EDITOR
if (!Application.isPlaying)
graphic.canvasRenderer.SetAlpha(m_IsOn ? 1f : 0f);
else
#endif
graphic.CrossFadeAlpha(m_IsOn ? 1f : 0f, instant ? 0f : 0.1f, true);
}
protected override void Start()
{
PlayEffect(true);
}
private void InternalToggle()
{
if (!IsActive() || !IsInteractable())
return;
isOn = !isOn;
}
public override void OnPointerEnter(PointerEventData eventData)
{
base.OnPointerEnter(eventData);
PlayAudio(hoverAudioClip);
}
public virtual void OnPointerClick(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
InternalToggle();
PlayAudio(clickAudioClip);
}
public virtual void OnSubmit(BaseEventData eventData)
{
InternalToggle();
PlayAudio(clickAudioClip);
}
private void PlayAudio(AudioClip clip)
{
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
}
#if INPUTSYSTEM_SUPPORT
InputActionReference IHotkeyTrigger.HotkeyAction
{
get => _hotkeyAction;
set => _hotkeyAction = value;
}
EHotkeyPressType IHotkeyTrigger.HotkeyPressType
{
get => _hotkeyPressType;
set => _hotkeyPressType = value;
}
void IHotkeyTrigger.HotkeyActionTrigger()
{
if (interactable)
{
OnSubmit(null);
}
}
[SerializeField] internal InputActionReference _hotkeyAction;
[SerializeField] internal EHotkeyPressType _hotkeyPressType;
public InputActionReference HotKeyRefrence
{
get { return _hotkeyAction; }
}
#endif
[SerializeField] private AudioClip hoverAudioClip;
[SerializeField] private AudioClip clickAudioClip;
}
}

View File

@ -1,11 +1,11 @@
fileFormatVersion: 2
guid: 42ae64be990942c899bebfffed4b48a5
guid: 938f629f24004544a889b461e6b7560b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 337b039db051cab44819dc51e6af1f43, type: 3}
icon: {fileID: 2800000, guid: ead857c5c78826747a7ab33355cd8108, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,10 +1,10 @@
#if INPUTSYSTEM_SUPPORT
using System.Linq;
using AlicizaX.UI.Runtime;
using UnityEngine;
namespace AlicizaX.UI
namespace UnityEngine.UI
{
// [DisallowMultipleComponent]
public class HotkeyBindComponent : MonoBehaviour
{
private UIHolderObjectBase _holderObjectBase;
@ -18,17 +18,25 @@ namespace AlicizaX.UI
private void OnDestroy()
{
_holderObjectBase.OnWindowBeforeShowEvent -= BindHotKeys;
_holderObjectBase.OnWindowBeforeClosedEvent -= UnBindHotKeys;
if (_holderObjectBase != null)
{
_holderObjectBase.OnWindowBeforeShowEvent -= BindHotKeys;
_holderObjectBase.OnWindowBeforeClosedEvent -= UnBindHotKeys;
}
}
[SerializeField] private UXHotkeyButton[] hotButtons;
// 改成 Component[](或 MonoBehaviour[]Unity 可以序列化 Component 引用
[SerializeField] private Component[] hotButtons;
internal void BindHotKeys()
{
if (hotButtons == null) return;
for (int i = 0; i < hotButtons.Length; i++)
{
hotButtons[i].BindHotKey();
if (hotButtons[i] is IHotkeyTrigger trigger)
{
trigger.BindHotKey();
}
}
}
@ -36,18 +44,41 @@ namespace AlicizaX.UI
[ContextMenu("Bind HotKeys")]
private void CollectUXHotkeys()
{
hotButtons = gameObject.GetComponentsInChildren<UXHotkeyButton>(true);
// 更稳健的查找:先拿所有 MonoBehaviour再 OfType 接口
var found = gameObject
.GetComponentsInChildren<MonoBehaviour>(true)
.OfType<IHotkeyTrigger>()
.Where(t => t.HotkeyAction != null) // 保留原来的筛选条件(如果 HotkeyAction 存在)
.Select(t => t as Component)
.ToArray();
hotButtons = found;
Debug.Log($"[HotkeyBindComponent] 已找到 {hotButtons.Length} 个 UXHotkey 组件并绑定。");
}
private void OnValidate()
{
if (_holderObjectBase == null)
{
_holderObjectBase = gameObject.GetComponent<UIHolderObjectBase>();
}
// 在编辑器模式下自动收集(可根据需求移除)
CollectUXHotkeys();
}
#endif
internal void UnBindHotKeys()
{
if (hotButtons == null) return;
for (int i = 0; i < hotButtons.Length; i++)
{
hotButtons[i].UnBindHotKey();
if (hotButtons[i] is IHotkeyTrigger trigger)
{
trigger.UnBindHotKey();
}
}
}
}
}
#endif

View File

@ -0,0 +1,14 @@
#if INPUTSYSTEM_SUPPORT
using UnityEngine.InputSystem;
namespace UnityEngine.UI
{
public interface IHotkeyTrigger
{
internal InputActionReference HotkeyAction { get; set; }
internal EHotkeyPressType HotkeyPressType { get; set; }
internal void HotkeyActionTrigger();
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1b8deefaf6dc42559bed29ea82e80164
timeCreated: 1766142335

View File

@ -1,28 +0,0 @@
#if INPUTSYSTEM_SUPPORT
using UnityEngine;
using UnityEngine.InputSystem;
namespace AlicizaX.UI
{
[DisallowMultipleComponent]
public class UXHotkeyButton : UXButton
{
[SerializeField] internal InputActionReference _hotKeyRefrence;
[SerializeField] internal EHotkeyPressType _hotkeyPressType;
public InputActionReference HotKeyRefrence
{
get { return _hotKeyRefrence; }
}
internal void HotkeyActionTrigger()
{
if (Interactable)
{
OnSubmit(null);
}
}
}
}
#endif

View File

@ -3,10 +3,9 @@ using System;
using UnityEngine;
using UnityEngine.InputSystem;
using System.Collections.Generic;
using System.Collections;
using AlicizaX.UI;
using UnityEngine.UI;
namespace AlicizaX.UI
namespace UnityEngine.UI
{
internal enum EHotkeyPressType
{
@ -19,9 +18,9 @@ namespace AlicizaX.UI
private readonly struct HotkeyRegistration
{
public readonly EHotkeyPressType pressType;
public readonly UXHotkeyButton button;
public readonly IHotkeyTrigger button;
public HotkeyRegistration(UXHotkeyButton btn, EHotkeyPressType pressType)
public HotkeyRegistration(IHotkeyTrigger btn, EHotkeyPressType pressType)
{
button = btn;
this.pressType = pressType;
@ -35,7 +34,7 @@ namespace AlicizaX.UI
new(32);
private static readonly Dictionary<UXHotkeyButton, string> _buttonRegistrations = new(64);
private static readonly Dictionary<IHotkeyTrigger, string> _buttonRegistrations = new(64);
#if UNITY_EDITOR
@ -53,7 +52,7 @@ namespace AlicizaX.UI
}
#endif
internal static void RegisterHotkey(UXHotkeyButton button, InputActionReference action, EHotkeyPressType pressType)
internal static void RegisterHotkey(IHotkeyTrigger button, InputActionReference action, EHotkeyPressType pressType)
{
if (action == null || action.action == null || button == null)
return;
@ -94,7 +93,7 @@ namespace AlicizaX.UI
}
}
public static void UnregisterHotkey(UXHotkeyButton button)
public static void UnregisterHotkey(IHotkeyTrigger button)
{
if (button == null || !_buttonRegistrations.TryGetValue(button, out var actionId))
return;
@ -153,24 +152,24 @@ namespace AlicizaX.UI
public static class UXHotkeyHotkeyExtension
{
public static void BindHotKey(this UXHotkeyButton button)
public static void BindHotKey(this IHotkeyTrigger button)
{
if (button == null) return;
if (button._hotKeyRefrence != null)
if (button.HotkeyAction != null)
{
UXHotkeyRegisterManager.RegisterHotkey(
button,
button._hotKeyRefrence,
button._hotkeyPressType
button.HotkeyAction,
button.HotkeyPressType
);
}
}
public static void UnBindHotKey(this UXHotkeyButton button)
public static void UnBindHotKey(this IHotkeyTrigger button)
{
if (button == null || button._hotKeyRefrence == null) return;
if (button == null || button.HotkeyAction == null) return;
UXHotkeyRegisterManager.UnregisterHotkey(button);
}
}

View File

@ -1,133 +0,0 @@
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
internal static class MultipleDisplayUtilities
{
/// <summary>
/// Converts the current drag position into a relative position for the display.
/// </summary>
/// <param name="eventData"></param>
/// <param name="position"></param>
/// <returns>Returns true except when the drag operation is not on the same display as it originated</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
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);
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f5fbd4c3b1c84b51abda86322e214b05
timeCreated: 1765260757

View File

@ -1,35 +0,0 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityEngine.UI
{
internal static class SetPropertyUtility
{
public static bool SetColor(ref Color currentValue, Color newValue)
{
if (currentValue.r == newValue.r && currentValue.g == newValue.g && currentValue.b == newValue.b && currentValue.a == newValue.a)
return false;
currentValue = newValue;
return true;
}
public static bool SetStruct<T>(ref T currentValue, T newValue) where T : struct
{
if (EqualityComparer<T>.Default.Equals(currentValue, newValue))
return false;
currentValue = newValue;
return true;
}
public static bool SetClass<T>(ref T currentValue, T newValue) where T : class
{
if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
return false;
currentValue = newValue;
return true;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: d3d40fd183ef410e995334bdca8e4638
timeCreated: 1764668263

View File

@ -1,85 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace UnityEngine.UI
{
[Serializable]
public struct UXNavigation : IEquatable<UXNavigation>
{
[Flags]
public enum Mode
{
None = 0,
Horizontal = 1,
Vertical = 2,
Automatic = 3,
Explicit = 4,
}
[SerializeField] private Mode m_Mode;
[HideInInspector] [SerializeField] private bool m_WrapAround;
[SerializeField] private UXSelectable m_SelectOnUp;
[SerializeField] private UXSelectable m_SelectOnDown;
[SerializeField] private UXSelectable m_SelectOnLeft;
[SerializeField] private UXSelectable m_SelectOnRight;
public Mode mode
{
get { return m_Mode; }
set { m_Mode = value; }
}
public bool wrapAround
{
get { return m_WrapAround; }
set { m_WrapAround = value; }
}
public UXSelectable selectOnUp
{
get { return m_SelectOnUp; }
set { m_SelectOnUp = value; }
}
public UXSelectable selectOnDown
{
get { return m_SelectOnDown; }
set { m_SelectOnDown = value; }
}
public UXSelectable selectOnLeft
{
get { return m_SelectOnLeft; }
set { m_SelectOnLeft = value; }
}
public UXSelectable selectOnRight
{
get { return m_SelectOnRight; }
set { m_SelectOnRight = value; }
}
static public UXNavigation defaultNavigation
{
get
{
var defaultNav = new UXNavigation();
defaultNav.m_Mode = Mode.Automatic;
defaultNav.m_WrapAround = false;
return defaultNav;
}
}
public bool Equals(UXNavigation other)
{
return mode == other.mode &&
selectOnUp == other.selectOnUp &&
selectOnDown == other.selectOnDown &&
selectOnLeft == other.selectOnLeft &&
selectOnRight == other.selectOnRight;
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 46a321e196774407b8f17943e7b55c63
timeCreated: 1764667489

View File

@ -11,666 +11,107 @@ namespace UnityEngine.UI
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
public class UXSelectable : Selectable
{
[Serializable]
public enum SelectionState
[SerializeField] private List<TransitionData> m_ChildTransitions = new();
void StartChildColorTween(TransitionData transitionData, Color targetColor, bool instant)
{
Normal,
Highlighted,
Pressed,
Selected,
Disabled,
if (transitionData.targetGraphic == null)
return;
transitionData.targetGraphic.CrossFadeColor(targetColor, instant ? 0f : transitionData.colors.fadeDuration, true, true);
}
#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; }
protected bool m_GroupsAllowInteraction = true;
private readonly List<CanvasGroup> m_CanvasGroupCache = new List<CanvasGroup>();
// 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;
[SerializeField] private Animator _animator;
public Animator Animator => _animator ? _animator : _animator = GetComponent<Animator>();
public static readonly Dictionary<string, int> _animTriggerCache = new()
void DoChildSpriteSwap(TransitionData transitionData, Sprite newSprite)
{
{ "Normal", Animator.StringToHash("Normal") },
{ "Highlighted", Animator.StringToHash("Highlighted") },
{ "Pressed", Animator.StringToHash("Pressed") },
{ "Selected", Animator.StringToHash("Selected") },
{ "Disabled", Animator.StringToHash("Disabled") },
};
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();
}
}
public bool Interactable
{
get => m_Interactable;
set
{
if (m_Interactable == value) return;
m_Interactable = value;
OnSetProperty();
}
}
public static UXSelectable[] allSelectablesArray
{
get
{
UXSelectable[] temp = new UXSelectable[s_SelectableCount];
Array.Copy(s_Selectables, temp, s_SelectableCount);
return temp;
}
}
#endregion
#region Unity Lifecycle
protected override void Awake()
{
base.Awake();
// nothing specific here; subclasses may initialize visuals
}
protected override void OnEnable()
{
if (m_EnableCalled)
if (transitionData.targetGraphic == null)
return;
base.OnEnable();
if (s_SelectableCount == s_Selectables.Length)
{
UXSelectable[] temp = new UXSelectable[s_Selectables.Length * 2];
Array.Copy(s_Selectables, temp, s_Selectables.Length);
s_Selectables = temp;
}
if (EventSystem.current && EventSystem.current.currentSelectedGameObject == gameObject)
{
hasSelection = true;
}
m_CurrentIndex = s_SelectableCount;
s_Selectables[m_CurrentIndex] = this;
s_SelectableCount++;
isPointerDown = false;
m_GroupsAllowInteraction = ParentGroupAllowsInteraction();
m_EnableCalled = true;
// Ensure visual state matches current settings immediately
OnSetProperty();
if (transitionData.targetGraphic is Image img)
img.overrideSprite = newSprite;
}
protected override void OnDisable()
{
if (!m_EnableCalled)
return;
s_SelectableCount--;
s_Selectables[s_SelectableCount].m_CurrentIndex = m_CurrentIndex;
s_Selectables[m_CurrentIndex] = s_Selectables[s_SelectableCount];
s_Selectables[s_SelectableCount] = null;
InstantClearState();
base.OnDisable();
m_EnableCalled = false;
}
protected void OnCanvasGroupChanged()
{
var parentGroupAllows = ParentGroupAllowsInteraction();
if (parentGroupAllows != m_GroupsAllowInteraction)
{
m_GroupsAllowInteraction = parentGroupAllows;
OnSetProperty();
}
}
#endregion
#region Groups & Interactable
protected bool ParentGroupAllowsInteraction()
{
Transform t = transform;
while (t != null)
{
t.GetComponents(m_CanvasGroupCache);
for (var i = 0; i < m_CanvasGroupCache.Count; i++)
{
if (m_CanvasGroupCache[i].enabled && !m_CanvasGroupCache[i].interactable)
return false;
if (m_CanvasGroupCache[i].ignoreParentGroups)
return true;
}
t = t.parent;
}
return true;
}
public virtual bool IsInteractable()
{
return m_GroupsAllowInteraction && m_Interactable;
}
#endregion
#region Property change handling & SelectionState API
/// <summary>
/// Called when a property that affects visuals changes (navigation/interactable/animation/etc).
/// </summary>
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);
}
}
/// <summary>
/// Computed selection state based on pointer / selection flags and interactability.
/// Mirrors Unity's Selectable.currentSelectionState behavior.
/// </summary>
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;
}
}
/// <summary>
/// Evaluate and transition to the computed selection state. Call after changing pointer/selection flags.
/// </summary>
protected void EvaluateAndTransitionToSelectionState()
{
if (!IsActive() || !IsInteractable())
return;
DoStateTransition(currentSelectionState, false);
}
/// <summary>
/// Force apply visual state immediately.
/// </summary>
protected void ForceSetState(SelectionState state)
{
m_SelectionState = state;
DoStateTransition(state, true);
}
/// <summary>
/// Clear internal state and restore visuals (used on disable / focus lost).
/// </summary>
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;
}
/// <summary>
/// Core mapping from SelectionState -> TransitionData and execution.
/// Subclasses can override PlayAnimation or ApplyVisualState for custom behavior.
/// </summary>
/// <param name="state"></param>
/// <param name="instant"></param>
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;
Vector3 localDir = Quaternion.Inverse(transform.rotation) * dir;
Vector3 pos = transform.TransformPoint(GetPointOnRectEdge(transform as RectTransform, localDir));
float maxScore = Mathf.NegativeInfinity;
float maxFurthestScore = Mathf.NegativeInfinity;
float score = 0;
bool wantsWrapAround = navigation.wrapAround && (m_Navigation.mode == UXNavigation.Mode.Vertical || m_Navigation.mode == UXNavigation.Mode.Horizontal);
UXSelectable bestPick = null;
UXSelectable bestFurthestPick = null;
for (int i = 0; i < s_SelectableCount; ++i)
{
UXSelectable sel = s_Selectables[i];
if (sel == this)
continue;
if (!sel.IsInteractable() || sel.navigation.mode == UXNavigation.Mode.None)
continue;
#if UNITY_EDITOR
if (Camera.current != null && !UnityEditor.SceneManagement.StageUtility.IsGameObjectRenderedByCamera(sel.gameObject, Camera.current))
continue;
protected override void OnValidate()
{
if (isActiveAndEnabled)
{
for (int i = 0; i < m_ChildTransitions.Count; i++)
{
DoChildSpriteSwap(m_ChildTransitions[i], null);
StartChildColorTween(m_ChildTransitions[i], Color.white, true);
}
}
base.OnValidate();
}
#endif
var selRect = sel.transform as RectTransform;
Vector3 selCenter = selRect != null ? (Vector3)selRect.rect.center : Vector3.zero;
Vector3 myVector = sel.transform.TransformPoint(selCenter) - pos;
float dot = Vector3.Dot(dir, myVector);
if (wantsWrapAround && dot < 0)
protected override void InstantClearState()
{
base.InstantClearState();
for (int i = 0; i < m_ChildTransitions.Count; i++)
{
switch (transition)
{
score = -dot * myVector.sqrMagnitude;
if (score > maxFurthestScore)
{
maxFurthestScore = score;
bestFurthestPick = sel;
}
continue;
}
if (dot <= 0)
continue;
score = dot / myVector.sqrMagnitude;
if (score > maxScore)
{
maxScore = score;
bestPick = sel;
case Transition.ColorTint:
StartChildColorTween(m_ChildTransitions[i], Color.white, true);
break;
case Transition.SpriteSwap:
DoChildSpriteSwap(m_ChildTransitions[i], null);
break;
}
}
if (wantsWrapAround && null == bestPick) return bestFurthestPick;
return bestPick;
}
private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir)
protected override void DoStateTransition(SelectionState state, bool instant)
{
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;
}
base.DoStateTransition(state, instant);
public virtual UXSelectable FindSelectableOnLeft()
{
if (m_Navigation.mode == UXNavigation.Mode.Explicit)
if (m_Navigation.selectOnLeft != null && m_Navigation.selectOnLeft.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None)
return m_Navigation.selectOnLeft;
if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0)
return FindSelectable(transform.rotation * Vector3.left);
return null;
}
public virtual UXSelectable FindSelectableOnRight()
{
if (m_Navigation.mode == UXNavigation.Mode.Explicit)
if (m_Navigation.selectOnRight != null && m_Navigation.selectOnRight.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None)
return m_Navigation.selectOnRight;
if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0)
return FindSelectable(transform.rotation * Vector3.right);
return null;
}
public virtual UXSelectable FindSelectableOnUp()
{
if (m_Navigation.mode == UXNavigation.Mode.Explicit)
if (m_Navigation.selectOnUp != null && m_Navigation.selectOnUp.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None)
return m_Navigation.selectOnUp;
if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0)
return FindSelectable(transform.rotation * Vector3.up);
return null;
}
public virtual UXSelectable FindSelectableOnDown()
{
if (m_Navigation.mode == UXNavigation.Mode.Explicit)
if (m_Navigation.selectOnDown != null && m_Navigation.selectOnDown.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None)
return m_Navigation.selectOnDown;
if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0)
return FindSelectable(transform.rotation * Vector3.down);
return null;
}
public virtual void OnMove(AxisEventData eventData)
{
switch (eventData.moveDir)
for (int i = 0; i < m_ChildTransitions.Count; i++)
{
case MoveDirection.Right:
Navigate(eventData, FindSelectableOnRight());
break;
case MoveDirection.Up:
Navigate(eventData, FindSelectableOnUp());
break;
case MoveDirection.Left:
Navigate(eventData, FindSelectableOnLeft());
break;
case MoveDirection.Down:
Navigate(eventData, FindSelectableOnDown());
break;
TransitionData transitionData = m_ChildTransitions[i];
Color tintColor;
Sprite transitionSprite;
switch (state)
{
case SelectionState.Normal:
tintColor = transitionData.colors.normalColor;
transitionSprite = null;
break;
case SelectionState.Highlighted:
tintColor = transitionData.colors.highlightedColor;
transitionSprite = transitionData.spriteState.highlightedSprite;
break;
case SelectionState.Pressed:
tintColor = transitionData.colors.pressedColor;
transitionSprite = transitionData.spriteState.pressedSprite;
break;
case SelectionState.Selected:
tintColor = transitionData.colors.selectedColor;
transitionSprite = transitionData.spriteState.selectedSprite;
break;
case SelectionState.Disabled:
tintColor = transitionData.colors.disabledColor;
transitionSprite = transitionData.spriteState.disabledSprite;
break;
default:
tintColor = Color.black;
transitionSprite = null;
break;
}
switch (transition)
{
case Transition.ColorTint:
StartChildColorTween(transitionData, tintColor * transitionData.colors.colorMultiplier, instant);
break;
case Transition.SpriteSwap:
DoChildSpriteSwap(transitionData, transitionSprite);
break;
}
}
}
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)
return;
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)
/// <summary>
/// High-level API: apply main transition visuals (child classes can override, UXButton overrides to add child transitions).
/// </summary>
/// <param name="state"></param>
/// <param name="instant"></param>
public virtual void ApplyVisualState(SelectionState state, bool instant)
{
if (m_MainTransition != null)
ApplyTransition(m_MainTransition, state, instant);
}
/// <summary>
/// Apply a single TransitionData (handles ColorTint / SpriteSwap / Animation).
/// Animation triggering calls PlayAnimation (virtual).
/// </summary>
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;
}
/// <summary>
/// Subclasses override this to trigger Animator triggers, etc.
/// </summary>
/// <param name="trigger"></param>
protected virtual void PlayAnimation(string trigger)
{
if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger) || !gameObject.activeInHierarchy)
return;
foreach (int id in _animTriggerCache.Values)
Animator.ResetTrigger(id);
if (_animTriggerCache.TryGetValue(trigger, out int hash))
Animator.SetTrigger(hash);
}
#endregion
}
}

File diff suppressed because it is too large Load Diff