381 lines
14 KiB
C#
381 lines
14 KiB
C#
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
|
|
using System.Collections.Generic;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace AlicizaX.UI.Extension.Editor
|
|
{
|
|
[CustomEditor(typeof(UXNavigationScope))]
|
|
public sealed class UXNavigationScopeEditor : UnityEditor.Editor
|
|
{
|
|
private readonly List<Selectable> _selectableBuffer = new List<Selectable>(64);
|
|
private SerializedProperty _defaultSelectable;
|
|
private SerializedProperty _bakedSelectables;
|
|
private SerializedProperty _runtimeSelectableCapacity;
|
|
private SerializedProperty _rememberLastSelection;
|
|
private SerializedProperty _requireSelectionWhenGamepad;
|
|
private SerializedProperty _blockLowerScopes;
|
|
private SerializedProperty _autoSelectFirstAvailable;
|
|
|
|
private void OnEnable()
|
|
{
|
|
_defaultSelectable = serializedObject.FindProperty("_defaultSelectable");
|
|
_bakedSelectables = serializedObject.FindProperty("_bakedSelectables");
|
|
_runtimeSelectableCapacity = serializedObject.FindProperty("_runtimeSelectableCapacity");
|
|
_rememberLastSelection = serializedObject.FindProperty("_rememberLastSelection");
|
|
_requireSelectionWhenGamepad = serializedObject.FindProperty("_requireSelectionWhenGamepad");
|
|
_blockLowerScopes = serializedObject.FindProperty("_blockLowerScopes");
|
|
_autoSelectFirstAvailable = serializedObject.FindProperty("_autoSelectFirstAvailable");
|
|
}
|
|
|
|
public override void OnInspectorGUI()
|
|
{
|
|
serializedObject.Update();
|
|
|
|
EditorGUILayout.PropertyField(_defaultSelectable);
|
|
EditorGUILayout.PropertyField(_runtimeSelectableCapacity);
|
|
EditorGUILayout.PropertyField(_rememberLastSelection);
|
|
EditorGUILayout.PropertyField(_requireSelectionWhenGamepad);
|
|
EditorGUILayout.PropertyField(_blockLowerScopes);
|
|
EditorGUILayout.PropertyField(_autoSelectFirstAvailable);
|
|
|
|
EditorGUILayout.Space();
|
|
DrawBakeTools();
|
|
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.PropertyField(_bakedSelectables, true);
|
|
|
|
EditorGUILayout.Space();
|
|
DrawDiagnostics();
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
}
|
|
|
|
private void DrawBakeTools()
|
|
{
|
|
using (new EditorGUILayout.HorizontalScope())
|
|
{
|
|
if (GUILayout.Button("收集本 Scope Selectable"))
|
|
{
|
|
BakeSelectables();
|
|
}
|
|
|
|
if (GUILayout.Button("清理空引用"))
|
|
{
|
|
RemoveNullEntries();
|
|
}
|
|
|
|
if (GUILayout.Button("按层级排序"))
|
|
{
|
|
SortBakedSelectables();
|
|
}
|
|
}
|
|
|
|
using (new EditorGUILayout.HorizontalScope())
|
|
{
|
|
if (GUILayout.Button("烘焙当前 Prefab 所有 Scope"))
|
|
{
|
|
BakeAllScopesInRoot();
|
|
}
|
|
|
|
if (GUILayout.Button("校验当前 Prefab 所有 Scope"))
|
|
{
|
|
ValidateAllScopesInRoot();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawDiagnostics()
|
|
{
|
|
UXNavigationScope scope = (UXNavigationScope)target;
|
|
EditorGUILayout.LabelField("诊断", EditorStyles.boldLabel);
|
|
EditorGUILayout.LabelField("烘焙控件数", _bakedSelectables.arraySize.ToString());
|
|
EditorGUILayout.LabelField("Runtime 动态控件数", Application.isPlaying ? scope.RuntimeSelectableCount.ToString() : "仅 Play Mode");
|
|
EditorGUILayout.LabelField("被 Skip", scope.GetComponentInParent<UXNavigationSkip>(true) != null ? "是" : "否");
|
|
EditorGUILayout.LabelField("当前 Suppressed", Application.isPlaying && scope.NavigationSuppressed ? "是" : "否");
|
|
ValidateReferences(scope);
|
|
}
|
|
|
|
private void ValidateReferences(UXNavigationScope scope)
|
|
{
|
|
Selectable defaultSelectable = _defaultSelectable.objectReferenceValue as Selectable;
|
|
if (defaultSelectable != null && defaultSelectable.GetComponentInParent<UXNavigationScope>(true) != scope)
|
|
{
|
|
EditorGUILayout.HelpBox("默认选中控件不属于当前 UXNavigationScope。", MessageType.Error);
|
|
}
|
|
|
|
for (int i = 0; i < _bakedSelectables.arraySize; i++)
|
|
{
|
|
Selectable selectable = _bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue as Selectable;
|
|
if (selectable == null)
|
|
{
|
|
EditorGUILayout.HelpBox("烘焙列表存在空引用。", MessageType.Warning);
|
|
continue;
|
|
}
|
|
|
|
if (selectable.GetComponentInParent<UXNavigationScope>(true) != scope)
|
|
{
|
|
EditorGUILayout.HelpBox("烘焙列表存在跨 Scope 引用。", MessageType.Error);
|
|
return;
|
|
}
|
|
|
|
for (int j = i + 1; j < _bakedSelectables.arraySize; j++)
|
|
{
|
|
if (_bakedSelectables.GetArrayElementAtIndex(j).objectReferenceValue == selectable)
|
|
{
|
|
EditorGUILayout.HelpBox("烘焙列表存在重复引用。", MessageType.Error);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void BakeSelectables()
|
|
{
|
|
UXNavigationScope scope = (UXNavigationScope)target;
|
|
BakeScope(scope, serializedObject, _bakedSelectables, _selectableBuffer);
|
|
}
|
|
|
|
private void RemoveNullEntries()
|
|
{
|
|
UXNavigationScope scope = (UXNavigationScope)target;
|
|
Undo.RecordObject(scope, "Clean UX Navigation Selectables");
|
|
for (int i = _bakedSelectables.arraySize - 1; i >= 0; i--)
|
|
{
|
|
if (_bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue == null)
|
|
{
|
|
_bakedSelectables.DeleteArrayElementAtIndex(i);
|
|
}
|
|
}
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
EditorUtility.SetDirty(scope);
|
|
}
|
|
|
|
private void SortBakedSelectables()
|
|
{
|
|
UXNavigationScope scope = (UXNavigationScope)target;
|
|
SortScope(scope, serializedObject, _bakedSelectables);
|
|
}
|
|
|
|
private void BakeAllScopesInRoot()
|
|
{
|
|
GameObject root = GetRootGameObject(((UXNavigationScope)target).gameObject);
|
|
UXNavigationScope[] scopes = root.GetComponentsInChildren<UXNavigationScope>(true);
|
|
for (int i = 0; i < scopes.Length; i++)
|
|
{
|
|
UXNavigationScope scope = scopes[i];
|
|
SerializedObject scopeObject = new SerializedObject(scope);
|
|
SerializedProperty bakedSelectables = scopeObject.FindProperty("_bakedSelectables");
|
|
BakeScope(scope, scopeObject, bakedSelectables);
|
|
}
|
|
}
|
|
|
|
private void ValidateAllScopesInRoot()
|
|
{
|
|
GameObject root = GetRootGameObject(((UXNavigationScope)target).gameObject);
|
|
UXNavigationScope[] scopes = root.GetComponentsInChildren<UXNavigationScope>(true);
|
|
int errorCount = 0;
|
|
for (int i = 0; i < scopes.Length; i++)
|
|
{
|
|
if (!ValidateScope(scopes[i]))
|
|
{
|
|
errorCount++;
|
|
}
|
|
}
|
|
|
|
if (errorCount == 0)
|
|
{
|
|
Debug.Log("UXNavigationScope validation passed.", root);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogErrorFormat(root, "UXNavigationScope validation failed. Error count: {0}", errorCount);
|
|
}
|
|
}
|
|
|
|
private static readonly List<Selectable> StaticSelectableBuffer = new List<Selectable>(64);
|
|
|
|
private static void BakeScope(UXNavigationScope scope, SerializedObject scopeObject, SerializedProperty bakedSelectables)
|
|
{
|
|
BakeScope(scope, scopeObject, bakedSelectables, StaticSelectableBuffer);
|
|
}
|
|
|
|
private static void BakeScope(UXNavigationScope scope, SerializedObject scopeObject, SerializedProperty bakedSelectables, List<Selectable> scopeEditorBuffer)
|
|
{
|
|
Selectable[] allSelectables = scope.GetComponentsInChildren<Selectable>(true);
|
|
List<Selectable> ownedSelectables = scopeEditorBuffer;
|
|
ownedSelectables.Clear();
|
|
for (int i = 0; i < allSelectables.Length; i++)
|
|
{
|
|
Selectable selectable = allSelectables[i];
|
|
if (selectable != null && selectable.GetComponentInParent<UXNavigationScope>(true) == scope)
|
|
{
|
|
ownedSelectables.Add(selectable);
|
|
}
|
|
}
|
|
|
|
ownedSelectables.Sort(CompareSiblingPath);
|
|
Undo.RecordObject(scope, "Bake UX Navigation Selectables");
|
|
bakedSelectables.arraySize = ownedSelectables.Count;
|
|
for (int i = 0; i < ownedSelectables.Count; i++)
|
|
{
|
|
bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue = ownedSelectables[i];
|
|
}
|
|
|
|
scopeObject.ApplyModifiedProperties();
|
|
EditorUtility.SetDirty(scope);
|
|
}
|
|
|
|
private static void SortScope(UXNavigationScope scope, SerializedObject scopeObject, SerializedProperty bakedSelectables)
|
|
{
|
|
List<Selectable> selectables = StaticSelectableBuffer;
|
|
selectables.Clear();
|
|
for (int i = 0; i < bakedSelectables.arraySize; i++)
|
|
{
|
|
Selectable selectable = bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue as Selectable;
|
|
if (selectable != null)
|
|
{
|
|
selectables.Add(selectable);
|
|
}
|
|
}
|
|
|
|
selectables.Sort(CompareSiblingPath);
|
|
Undo.RecordObject(scope, "Sort UX Navigation Selectables");
|
|
bakedSelectables.arraySize = selectables.Count;
|
|
for (int i = 0; i < selectables.Count; i++)
|
|
{
|
|
bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue = selectables[i];
|
|
}
|
|
|
|
scopeObject.ApplyModifiedProperties();
|
|
EditorUtility.SetDirty(scope);
|
|
}
|
|
|
|
private static bool ValidateScope(UXNavigationScope scope)
|
|
{
|
|
SerializedObject scopeObject = new SerializedObject(scope);
|
|
SerializedProperty defaultSelectableProperty = scopeObject.FindProperty("_defaultSelectable");
|
|
SerializedProperty bakedSelectables = scopeObject.FindProperty("_bakedSelectables");
|
|
Selectable defaultSelectable = defaultSelectableProperty.objectReferenceValue as Selectable;
|
|
bool valid = true;
|
|
|
|
if (defaultSelectable != null && defaultSelectable.GetComponentInParent<UXNavigationScope>(true) != scope)
|
|
{
|
|
Debug.LogError("UXNavigationScope default selectable crosses scope.", scope);
|
|
valid = false;
|
|
}
|
|
|
|
for (int i = 0; i < bakedSelectables.arraySize; i++)
|
|
{
|
|
Selectable selectable = bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue as Selectable;
|
|
if (selectable == null)
|
|
{
|
|
Debug.LogWarning("UXNavigationScope baked selectables contain null entry.", scope);
|
|
valid = false;
|
|
continue;
|
|
}
|
|
|
|
if (selectable.GetComponentInParent<UXNavigationScope>(true) != scope)
|
|
{
|
|
Debug.LogError("UXNavigationScope baked selectable crosses scope.", selectable);
|
|
valid = false;
|
|
}
|
|
|
|
for (int j = i + 1; j < bakedSelectables.arraySize; j++)
|
|
{
|
|
if (bakedSelectables.GetArrayElementAtIndex(j).objectReferenceValue == selectable)
|
|
{
|
|
Debug.LogError("UXNavigationScope baked selectables contain duplicate entry.", selectable);
|
|
valid = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
private static GameObject GetRootGameObject(GameObject gameObject)
|
|
{
|
|
Transform current = gameObject.transform;
|
|
while (current.parent != null)
|
|
{
|
|
current = current.parent;
|
|
}
|
|
|
|
return current.gameObject;
|
|
}
|
|
|
|
private static int CompareSiblingPath(Selectable left, Selectable right)
|
|
{
|
|
if (left == right)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
Transform leftTransform = left != null ? left.transform : null;
|
|
Transform rightTransform = right != null ? right.transform : null;
|
|
return CompareSiblingPath(leftTransform, rightTransform);
|
|
}
|
|
|
|
private static int CompareSiblingPath(Transform left, Transform right)
|
|
{
|
|
if (left == right)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (left == null)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if (right == null)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
int leftDepth = GetDepth(left);
|
|
int rightDepth = GetDepth(right);
|
|
Transform leftCursor = left;
|
|
Transform rightCursor = right;
|
|
|
|
while (leftDepth > rightDepth)
|
|
{
|
|
leftCursor = leftCursor.parent;
|
|
leftDepth--;
|
|
}
|
|
|
|
while (rightDepth > leftDepth)
|
|
{
|
|
rightCursor = rightCursor.parent;
|
|
rightDepth--;
|
|
}
|
|
|
|
while (leftCursor.parent != rightCursor.parent)
|
|
{
|
|
leftCursor = leftCursor.parent;
|
|
rightCursor = rightCursor.parent;
|
|
}
|
|
|
|
return leftCursor.GetSiblingIndex().CompareTo(rightCursor.GetSiblingIndex());
|
|
}
|
|
|
|
private static int GetDepth(Transform transform)
|
|
{
|
|
int depth = 0;
|
|
Transform current = transform;
|
|
while (current != null)
|
|
{
|
|
depth++;
|
|
current = current.parent;
|
|
}
|
|
|
|
return depth;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|