大优化
大优化 懒得写了 重构了分离了 UXButton UXToggle UXSelectable UXGroup
This commit is contained in:
parent
64c8c338c6
commit
551423f09d
@ -3,7 +3,7 @@ using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AlicizaX.UI.Extension
|
||||
namespace UnityEditor.Extensions
|
||||
{
|
||||
internal static class ExtensionHelper
|
||||
{
|
||||
|
||||
@ -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));
|
||||
|
||||
170
Editor/Inspector/TabbedInspector.cs
Normal file
170
Editor/Inspector/TabbedInspector.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Editor/Inspector/TabbedInspector.cs.meta
Normal file
3
Editor/Inspector/TabbedInspector.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2033905cec9b4aaeadee445667ca163b
|
||||
timeCreated: 1766127466
|
||||
@ -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
|
||||
|
||||
BIN
Editor/Res/ComponentIcon/Toggle Icon.png
Normal file
BIN
Editor/Res/ComponentIcon/Toggle Icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 468 B |
127
Editor/Res/ComponentIcon/Toggle Icon.png.meta
Normal file
127
Editor/Res/ComponentIcon/Toggle Icon.png.meta
Normal 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:
|
||||
105
Editor/UX/Button/UButtonEditor.cs
Normal file
105
Editor/UX/Button/UButtonEditor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Editor/UX/Button/UButtonEditor.cs.meta
Normal file
3
Editor/UX/Button/UButtonEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d771091d826488b9ff5bc2799c11756
|
||||
timeCreated: 1766125678
|
||||
@ -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--;
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a6fb65845fb481293f57b30b1bfcb3b
|
||||
timeCreated: 1744275051
|
||||
270
Editor/UX/Group/UXGroupEditor.cs
Normal file
270
Editor/UX/Group/UXGroupEditor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc0b010e4f884f6992de90c5e5dd7154
|
||||
timeCreated: 1765185454
|
||||
@ -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>
|
||||
/// 子类调用:绘制基类的通用 Inspector(Navigation / 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 708a130e1d5f49f18619c6ad9c3baccb
|
||||
timeCreated: 1765260889
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e67f2a4d5c0a4f399f67374dcfd42a1f
|
||||
timeCreated: 1765260896
|
||||
3
Editor/UX/Toggle.meta
Normal file
3
Editor/UX/Toggle.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c51b9957a2b441edbcf2d1f264545312
|
||||
timeCreated: 1766136377
|
||||
178
Editor/UX/Toggle/UXToggleEditor.cs
Normal file
178
Editor/UX/Toggle/UXToggleEditor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Editor/UX/Toggle/UXToggleEditor.cs.meta
Normal file
3
Editor/UX/Toggle/UXToggleEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9301ee465f2c46d08b2fade637710625
|
||||
timeCreated: 1766136386
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
260
Runtime/UXComponent/Group/UXToggle.cs
Normal file
260
Runtime/UXComponent/Group/UXToggle.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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:
|
||||
@ -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
|
||||
|
||||
14
Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs
Normal file
14
Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs
Normal 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
|
||||
3
Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs.meta
Normal file
3
Runtime/UXComponent/Hotkey/IHotkeyTrigger.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b8deefaf6dc42559bed29ea82e80164
|
||||
timeCreated: 1766142335
|
||||
@ -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
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5fbd4c3b1c84b51abda86322e214b05
|
||||
timeCreated: 1765260757
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3d40fd183ef410e995334bdca8e4638
|
||||
timeCreated: 1764668263
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46a321e196774407b8f17943e7b55c63
|
||||
timeCreated: 1764667489
|
||||
@ -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
Loading…
Reference in New Issue
Block a user