com.alicizax.unity.ui.exten.../Editor/UX/Navigation/UXNavigationScopeEditor.cs

381 lines
14 KiB
C#
Raw Normal View History

#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