#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 _selectableBuffer = new List(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(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(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(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(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(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 StaticSelectableBuffer = new List(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 scopeEditorBuffer) { Selectable[] allSelectables = scope.GetComponentsInChildren(true); List ownedSelectables = scopeEditorBuffer; ownedSelectables.Clear(); for (int i = 0; i < allSelectables.Length; i++) { Selectable selectable = allSelectables[i]; if (selectable != null && selectable.GetComponentInParent(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 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(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(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