#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 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("收集子 Selectable")) { BakeSelectables(); } if (GUILayout.Button("清理空引用")) { RemoveNullEntries(); } if (GUILayout.Button("按层级排序")) { SortBakedSelectables(); } } } 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; } } } private void BakeSelectables() { UXNavigationScope scope = (UXNavigationScope)target; Selectable[] allSelectables = scope.GetComponentsInChildren(true); List ownedSelectables = new List(allSelectables.Length); for (int i = 0; i < allSelectables.Length; i++) { Selectable selectable = allSelectables[i]; if (selectable != null && selectable.GetComponentInParent(true) == scope) { ownedSelectables.Add(selectable); } } 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]; } serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(scope); } 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; List selectables = new List(_bakedSelectables.arraySize); 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]; } serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(scope); } private static int CompareSiblingPath(Selectable left, Selectable right) { if (left == right) { return 0; } string leftPath = GetSiblingPath(left.transform); string rightPath = GetSiblingPath(right.transform); return string.CompareOrdinal(leftPath, rightPath); } private static string GetSiblingPath(Transform transform) { if (transform == null) { return string.Empty; } return transform.parent == null ? transform.GetSiblingIndex().ToString("D4") : GetSiblingPath(transform.parent) + "/" + transform.GetSiblingIndex().ToString("D4"); } } } #endif