优化UIExtension扩展系统

优化UXBinding编辑器
优化UXBinding性能
优化UXBinding Bug
优化Hotkey注册器性能
优化UXNavigation导航性能
This commit is contained in:
陈思海 2026-04-28 20:52:06 +08:00
parent 8c3c13634d
commit 526341579a
32 changed files with 3684 additions and 2745 deletions

View File

@ -8,27 +8,138 @@ namespace UnityEngine.UI
[CustomEditor(typeof(UXBinding))] [CustomEditor(typeof(UXBinding))]
public sealed class UXBindingEditor : UnityEditor.Editor public sealed class UXBindingEditor : UnityEditor.Editor
{ {
private readonly struct AddRuleOption
{
public readonly string ControllerId;
public readonly string ControllerName;
public readonly UXBindingProperty Property;
public readonly string PropertyName;
public AddRuleOption(string controllerId, string controllerName, UXBindingProperty property, string propertyName)
{
ControllerId = controllerId;
ControllerName = controllerName;
Property = property;
PropertyName = propertyName;
}
}
private sealed class AddRulePopup : PopupWindowContent
{
private readonly UXBindingEditor _editor;
private readonly UXBinding _binding;
private readonly List<AddRuleOption> _options;
private readonly bool _showControllerName;
private string _search = string.Empty;
private Vector2 _scroll;
public AddRulePopup(UXBindingEditor editor, UXBinding binding, List<AddRuleOption> options, bool showControllerName)
{
_editor = editor;
_binding = binding;
_options = new List<AddRuleOption>(options);
_showControllerName = showControllerName;
}
public override Vector2 GetWindowSize()
{
return new Vector2(360f, 320f);
}
public override void OnGUI(Rect rect)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Add Rule", EditorStyles.boldLabel);
EditorGUILayout.EndVertical();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
_search = GUILayout.TextField(_search, GUI.skin.FindStyle("ToolbarSearchTextField") ?? EditorStyles.toolbarSearchField, GUILayout.ExpandWidth(true));
if (GUILayout.Button(string.Empty, GUI.skin.FindStyle("ToolbarSearchCancelButton") ?? EditorStyles.toolbarButton, GUILayout.Width(18f)))
{
_search = string.Empty;
GUI.FocusControl(null);
}
EditorGUILayout.EndHorizontal();
_scroll = EditorGUILayout.BeginScrollView(_scroll);
for (int i = 0; i < _options.Count; i++)
{
AddRuleOption option = _options[i];
if (!IsMatch(option, _search))
{
continue;
}
string label = _showControllerName ? $"{option.ControllerName} / {option.PropertyName}" : option.PropertyName;
if (GUILayout.Button(label, EditorStyles.miniButton))
{
_editor.AddEntry(_binding, option.ControllerId, option.Property);
editorWindow.Close();
}
}
EditorGUILayout.EndScrollView();
}
private static bool IsMatch(AddRuleOption option, string search)
{
if (string.IsNullOrEmpty(search))
{
return true;
}
return option.ControllerName.IndexOf(search, System.StringComparison.OrdinalIgnoreCase) >= 0 ||
option.PropertyName.IndexOf(search, System.StringComparison.OrdinalIgnoreCase) >= 0;
}
}
private SerializedProperty _controllerProp; private SerializedProperty _controllerProp;
private SerializedProperty _entriesProp; private SerializedProperty _entriesProp;
private readonly Dictionary<int, bool> _foldouts = new Dictionary<int, bool>(); private readonly Dictionary<int, bool> _foldouts = new Dictionary<int, bool>();
private readonly List<UXBindingProperty> _supportedProperties = new List<UXBindingProperty>(); private readonly List<UXBindingProperty> _supportedProperties = new List<UXBindingProperty>();
private string[] _controllerNames = System.Array.Empty<string>();
private string[] _indexNames = System.Array.Empty<string>();
private GUIStyle _pillOn;
private GUIStyle _pillOff;
private readonly List<AddRuleOption> _addRuleOptions = new List<AddRuleOption>();
private GUIContent _addRuleContent;
private GUIContent _autoBindContent;
private GUIContent _captureContent;
private GUIContent _resetContent;
private GUIContent _expandAllContent;
private GUIContent _collapseAllContent;
private GUIContent _upContent;
private GUIContent _downContent;
private GUIContent _deleteContent;
private void OnEnable() private void OnEnable()
{ {
_controllerProp = serializedObject.FindProperty("_controller"); _controllerProp = serializedObject.FindProperty("_controller");
_entriesProp = serializedObject.FindProperty("_entries"); _entriesProp = serializedObject.FindProperty("_entries");
InitializeContents();
}
private void InitializeContents()
{
_addRuleContent = EditorGUIUtility.IconContent("Toolbar Plus", "Add binding rule");
_autoBindContent = EditorGUIUtility.IconContent("d_Prefab Icon", "Auto bind parent UXController");
_captureContent = EditorGUIUtility.IconContent("d_SaveAs", "Capture defaults");
_resetContent = EditorGUIUtility.IconContent("d_Refresh", "Reset to defaults");
_expandAllContent = EditorGUIUtility.IconContent("d_scrollup", "Expand all rules");
_collapseAllContent = EditorGUIUtility.IconContent("d_scrolldown", "Collapse all rules");
_upContent = EditorGUIUtility.IconContent("d_scrollup", "Move up");
_downContent = EditorGUIUtility.IconContent("d_scrolldown", "Move down");
_deleteContent = EditorGUIUtility.IconContent("TreeEditor.Trash", "Delete rule");
} }
public override void OnInspectorGUI() public override void OnInspectorGUI()
{ {
serializedObject.Update(); serializedObject.Update();
EnsureStyles();
var binding = (UXBinding)target; var binding = (UXBinding)target;
DrawHeader(binding); DrawHeader(binding);
EditorGUILayout.Space(6f); EditorGUILayout.Space(6f);
DrawControllerField(binding);
EditorGUILayout.Space(6f);
DrawEntries(binding); DrawEntries(binding);
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
@ -37,39 +148,11 @@ namespace UnityEngine.UI
private void DrawHeader(UXBinding binding) private void DrawHeader(UXBinding binding)
{ {
EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("UX Binding", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"Target: {binding.gameObject.name}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($"Rules: {_entriesProp.arraySize}", EditorStyles.miniLabel);
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Add Rule")) EditorGUILayout.LabelField("UX Binding", EditorStyles.boldLabel, GUILayout.Width(82f));
{
AddEntry(binding);
}
if (GUILayout.Button("Capture Defaults"))
{
binding.CaptureDefaults();
EditorUtility.SetDirty(binding);
}
if (GUILayout.Button("Reset To Defaults"))
{
binding.ResetToDefaults();
EditorUtility.SetDirty(binding);
SceneView.RepaintAll();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private void DrawControllerField(UXBinding binding)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Controller", EditorStyles.boldLabel);
EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck();
UXController newController = (UXController)EditorGUILayout.ObjectField("Reference", binding.Controller, typeof(UXController), true); UXController newController = (UXController)EditorGUILayout.ObjectField(binding.Controller, typeof(UXController), true);
if (EditorGUI.EndChangeCheck()) if (EditorGUI.EndChangeCheck())
{ {
Undo.RecordObject(binding, "Change UX Binding Controller"); Undo.RecordObject(binding, "Change UX Binding Controller");
@ -78,13 +161,40 @@ namespace UnityEngine.UI
EditorUtility.SetDirty(binding); EditorUtility.SetDirty(binding);
} }
if (binding.Controller != null)
{
EditorGUILayout.LabelField(binding.Controller.ControllerCount.ToString(), EditorStyles.miniLabel, GUILayout.Width(18f));
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(_addRuleContent, EditorStyles.miniButtonLeft, GUILayout.Width(28f)))
{
ShowAddRuleMenu(binding, GUILayoutUtility.GetLastRect());
}
if (GUILayout.Button(_autoBindContent, EditorStyles.miniButtonMid, GUILayout.Width(28f)))
{
AutoBindController(binding);
}
if (GUILayout.Button(_captureContent, EditorStyles.miniButtonMid, GUILayout.Width(28f)))
{
binding.CaptureDefaults();
EditorUtility.SetDirty(binding);
}
if (GUILayout.Button(_resetContent, EditorStyles.miniButtonRight, GUILayout.Width(28f)))
{
binding.ResetToDefaults();
EditorUtility.SetDirty(binding);
SceneView.RepaintAll();
}
EditorGUILayout.EndHorizontal();
if (binding.Controller == null) if (binding.Controller == null)
{ {
EditorGUILayout.HelpBox("Assign a UXController on this object or one of its parents.", MessageType.Warning); EditorGUILayout.HelpBox("Assign a UXController or use Auto Bind.", MessageType.Warning);
}
else
{
EditorGUILayout.LabelField($"Bound To: {binding.Controller.name}", EditorStyles.miniLabel);
} }
EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical();
@ -112,27 +222,39 @@ namespace UnityEngine.UI
SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index); SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index);
SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId"); SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId");
SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex"); SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex");
SerializedProperty controllerIndexMaskProp = entryProp.FindPropertyRelative("_controllerIndexMask");
SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property"); SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property");
SerializedProperty valueProp = entryProp.FindPropertyRelative("_value"); SerializedProperty valueProp = entryProp.FindPropertyRelative("_value");
SerializedProperty indexedValuesProp = entryProp.FindPropertyRelative("_indexedValues");
SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode"); SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode");
SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue"); SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue");
UXBindingProperty property = (UXBindingProperty)propertyProp.enumValueIndex; UXBindingProperty property = (UXBindingProperty)propertyProp.enumValueIndex;
bool expanded = _foldouts.ContainsKey(index) && _foldouts[index]; bool expanded = _foldouts.ContainsKey(index) && _foldouts[index];
string label = UXBindingPropertyUtility.GetMetadata(property).DisplayName; string label = UXBindingPropertyUtility.GetMetadata(property).DisplayName;
bool propertySupported = _supportedProperties.Contains(property);
bool controllerResolved = TryGetControllerName(binding.Controller, controllerIdProp.stringValue, out string controllerName);
EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
expanded = EditorGUILayout.Foldout(expanded, $"[{index}] {label}", true); expanded = EditorGUILayout.Foldout(expanded, $"[{index}] {label}", true);
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
if (GUILayout.Button("Preview", EditorStyles.miniButtonLeft, GUILayout.Width(60f))) EditorGUI.BeginDisabledGroup(index == 0);
if (GUILayout.Button(_upContent, EditorStyles.miniButtonLeft, GUILayout.Width(24f)))
{ {
binding.PreviewEntry(index); MoveEntry(index, index - 1);
SceneView.RepaintAll();
} }
EditorGUI.EndDisabledGroup();
if (GUILayout.Button("X", EditorStyles.miniButtonRight, GUILayout.Width(24f))) EditorGUI.BeginDisabledGroup(index >= _entriesProp.arraySize - 1);
if (GUILayout.Button(_downContent, EditorStyles.miniButtonMid, GUILayout.Width(24f)))
{
MoveEntry(index, index + 1);
}
EditorGUI.EndDisabledGroup();
if (GUILayout.Button(_deleteContent, EditorStyles.miniButtonRight, GUILayout.Width(24f)))
{ {
DeleteEntry(binding, index); DeleteEntry(binding, index);
} }
@ -141,39 +263,75 @@ namespace UnityEngine.UI
if (expanded) if (expanded)
{ {
DrawControllerSelector(binding, controllerIdProp, controllerIndexProp);
DrawPropertySelector(entryProp, binding.gameObject, propertyProp);
property = (UXBindingProperty)propertyProp.enumValueIndex;
UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(property); UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(property);
bool indexChanged = DrawControllerSelector(binding, controllerIdProp, controllerIndexProp, controllerIndexMaskProp, property == UXBindingProperty.GameObjectActive);
EditorGUILayout.Space(2f); if (property == UXBindingProperty.GameObjectActive)
EditorGUILayout.LabelField("Value", EditorStyles.boldLabel);
DrawValueField(valueProp, metadata, "Matched Value");
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Use Current"))
{ {
binding.CaptureEntryValue(index); DrawGameObjectActiveHint(controllerIndexMaskProp.intValue);
EditorUtility.SetDirty(binding); ForceGameObjectActiveValues(valueProp, fallbackModeProp, fallbackValueProp);
} if (indexChanged)
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(2f);
EditorGUILayout.PropertyField(fallbackModeProp, new GUIContent("Fallback"));
UXBindingFallbackMode fallbackMode = (UXBindingFallbackMode)fallbackModeProp.enumValueIndex;
if (fallbackMode == UXBindingFallbackMode.UseCustomValue)
{
DrawValueField(fallbackValueProp, metadata, "Fallback Value");
if (GUILayout.Button("Use Current As Fallback"))
{ {
binding.CaptureEntryFallbackValue(index); serializedObject.ApplyModifiedProperties();
binding.ApplyEntryValue(index, GetFirstSelectedIndex(controllerIndexMaskProp.intValue));
EditorUtility.SetDirty(binding); EditorUtility.SetDirty(binding);
SceneView.RepaintAll();
}
}
else
{
int selectedIndex = GetFirstSelectedIndex(controllerIndexMaskProp.intValue);
if (indexChanged)
{
serializedObject.ApplyModifiedProperties();
binding.ApplyEntryValue(index, selectedIndex);
EditorUtility.SetDirty(binding);
SceneView.RepaintAll();
}
EditorGUILayout.Space(2f);
EditorGUILayout.LabelField("Value", EditorStyles.boldLabel);
SerializedProperty selectedValueProp = GetIndexedValueProperty(indexedValuesProp, valueProp, selectedIndex);
EditorGUI.BeginChangeCheck();
DrawValueField(selectedValueProp, metadata, $"Index {selectedIndex} Value");
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
binding.ApplyEntryValue(index, selectedIndex);
EditorUtility.SetDirty(binding);
SceneView.RepaintAll();
}
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Use Current"))
{
binding.CaptureEntryValue(index, selectedIndex);
EditorUtility.SetDirty(binding);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(2f);
EditorGUILayout.PropertyField(fallbackModeProp, new GUIContent("Fallback"));
UXBindingFallbackMode fallbackMode = (UXBindingFallbackMode)fallbackModeProp.enumValueIndex;
if (fallbackMode == UXBindingFallbackMode.UseCustomValue)
{
DrawValueField(fallbackValueProp, metadata, "Fallback Value");
if (GUILayout.Button("Use Current As Fallback"))
{
binding.CaptureEntryFallbackValue(index);
EditorUtility.SetDirty(binding);
}
} }
} }
if (!_supportedProperties.Contains(property)) if (!controllerResolved)
{
EditorGUILayout.HelpBox("Controller reference is missing or points to a deleted controller definition.", MessageType.Error);
}
if (!propertySupported)
{ {
EditorGUILayout.HelpBox("This property is not supported by the components on the current GameObject.", MessageType.Error); EditorGUILayout.HelpBox("This property is not supported by the components on the current GameObject.", MessageType.Error);
} }
@ -182,22 +340,25 @@ namespace UnityEngine.UI
EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical();
} }
private void DrawControllerSelector(UXBinding binding, SerializedProperty controllerIdProp, SerializedProperty controllerIndexProp) private bool DrawControllerSelector(UXBinding binding, SerializedProperty controllerIdProp, SerializedProperty controllerIndexProp, SerializedProperty controllerIndexMaskProp, bool multiSelect)
{ {
UXController controller = binding.Controller; UXController controller = binding.Controller;
if (controller == null || controller.Controllers.Count == 0) if (controller == null || controller.Controllers.Count == 0)
{ {
EditorGUILayout.HelpBox("Create a controller definition first.", MessageType.Info); EditorGUILayout.HelpBox("Create a controller definition first.", MessageType.Info);
return; return false;
} }
string[] names = new string[controller.Controllers.Count]; bool changed = false;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Controller", EditorStyles.miniLabel, GUILayout.Width(64f));
EnsureStringArray(ref _controllerNames, controller.Controllers.Count);
int selectedController = 0; int selectedController = 0;
for (int i = 0; i < controller.Controllers.Count; i++) for (int i = 0; i < controller.Controllers.Count; i++)
{ {
UXController.ControllerDefinition definition = controller.Controllers[i]; UXController.ControllerDefinition definition = controller.Controllers[i];
names[i] = definition.Name; _controllerNames[i] = definition.Name;
if (definition.Id == controllerIdProp.stringValue) if (definition.Id == controllerIdProp.stringValue)
{ {
selectedController = i; selectedController = i;
@ -205,59 +366,23 @@ namespace UnityEngine.UI
} }
EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck();
selectedController = EditorGUILayout.Popup("Controller", selectedController, names); selectedController = EditorGUILayout.Popup(selectedController, _controllerNames, GUILayout.MinWidth(90f));
if (EditorGUI.EndChangeCheck()) if (EditorGUI.EndChangeCheck())
{ {
controllerIdProp.stringValue = controller.Controllers[selectedController].Id; controllerIdProp.stringValue = controller.Controllers[selectedController].Id;
controllerIndexProp.intValue = 0; controllerIndexProp.intValue = 0;
controllerIndexMaskProp.intValue = 1;
changed = true;
} }
UXController.ControllerDefinition selectedDefinition = controller.Controllers[selectedController]; UXController.ControllerDefinition selectedDefinition = controller.Controllers[selectedController];
int maxIndex = Mathf.Max(1, selectedDefinition.Length); int maxIndex = Mathf.Max(1, selectedDefinition.Length);
controllerIndexProp.intValue = Mathf.Clamp(controllerIndexProp.intValue, 0, maxIndex - 1); controllerIndexProp.intValue = Mathf.Clamp(controllerIndexProp.intValue, 0, maxIndex - 1);
controllerIndexMaskProp.intValue = ClampMask(controllerIndexMaskProp.intValue, maxIndex, controllerIndexProp.intValue);
string[] indexNames = new string[maxIndex]; changed |= DrawIndexMask(controllerIndexMaskProp, controllerIndexProp, maxIndex, multiSelect);
for (int i = 0; i < maxIndex; i++) EditorGUILayout.EndHorizontal();
{ return changed;
indexNames[i] = i.ToString();
}
controllerIndexProp.intValue = EditorGUILayout.Popup("Index", controllerIndexProp.intValue, indexNames);
}
private void DrawPropertySelector(SerializedProperty entryProp, GameObject targetObject, SerializedProperty propertyProp)
{
var options = new List<UXBindingProperty>(_supportedProperties);
UXBindingProperty current = (UXBindingProperty)propertyProp.enumValueIndex;
if (!options.Contains(current))
{
options.Add(current);
}
string[] displayNames = new string[options.Count];
int selectedIndex = 0;
for (int i = 0; i < options.Count; i++)
{
UXBindingProperty option = options[i];
UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(option);
bool supported = UXBindingPropertyUtility.IsSupported(targetObject, option);
displayNames[i] = supported ? metadata.DisplayName : $"{metadata.DisplayName} (Unsupported)";
if (option == current)
{
selectedIndex = i;
}
}
EditorGUI.BeginChangeCheck();
selectedIndex = EditorGUILayout.Popup("Property", selectedIndex, displayNames);
if (EditorGUI.EndChangeCheck())
{
propertyProp.enumValueIndex = (int)options[selectedIndex];
entryProp.FindPropertyRelative("_hasCapturedDefault").boolValue = false;
entryProp.FindPropertyRelative("_capturedProperty").enumValueIndex = propertyProp.enumValueIndex;
ResetValue(entryProp.FindPropertyRelative("_capturedDefault"));
ApplyDefaultFallbackForProperty(entryProp, (UXBindingProperty)propertyProp.enumValueIndex);
}
} }
private void DrawValueField(SerializedProperty valueProp, UXBindingPropertyMetadata metadata, string label) private void DrawValueField(SerializedProperty valueProp, UXBindingPropertyMetadata metadata, string label)
@ -314,7 +439,7 @@ namespace UnityEngine.UI
} }
} }
private void AddEntry(UXBinding binding) private void AddEntry(UXBinding binding, string controllerId, UXBindingProperty property)
{ {
int index = _entriesProp.arraySize; int index = _entriesProp.arraySize;
_entriesProp.InsertArrayElementAtIndex(index); _entriesProp.InsertArrayElementAtIndex(index);
@ -322,31 +447,73 @@ namespace UnityEngine.UI
SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index); SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index);
SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId"); SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId");
SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex"); SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex");
SerializedProperty controllerIndexMaskProp = entryProp.FindPropertyRelative("_controllerIndexMask");
SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property"); SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property");
SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode"); SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode");
SerializedProperty valueProp = entryProp.FindPropertyRelative("_value"); SerializedProperty valueProp = entryProp.FindPropertyRelative("_value");
SerializedProperty indexedValuesProp = entryProp.FindPropertyRelative("_indexedValues");
SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue"); SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue");
SerializedProperty capturedDefaultProp = entryProp.FindPropertyRelative("_capturedDefault"); SerializedProperty capturedDefaultProp = entryProp.FindPropertyRelative("_capturedDefault");
SerializedProperty hasCapturedDefaultProp = entryProp.FindPropertyRelative("_hasCapturedDefault"); SerializedProperty hasCapturedDefaultProp = entryProp.FindPropertyRelative("_hasCapturedDefault");
SerializedProperty capturedPropertyProp = entryProp.FindPropertyRelative("_capturedProperty"); SerializedProperty capturedPropertyProp = entryProp.FindPropertyRelative("_capturedProperty");
controllerIdProp.stringValue = string.Empty; controllerIdProp.stringValue = controllerId;
controllerIndexProp.intValue = 0; controllerIndexProp.intValue = 0;
propertyProp.enumValueIndex = (int)UXBindingProperty.GameObjectActive; controllerIndexMaskProp.intValue = 1;
propertyProp.enumValueIndex = (int)property;
fallbackModeProp.enumValueIndex = (int)UXBindingFallbackMode.RestoreCapturedDefault; fallbackModeProp.enumValueIndex = (int)UXBindingFallbackMode.RestoreCapturedDefault;
ResetValue(valueProp); ResetValue(valueProp);
indexedValuesProp.ClearArray();
ResetValue(fallbackValueProp); ResetValue(fallbackValueProp);
ResetValue(capturedDefaultProp); ResetValue(capturedDefaultProp);
hasCapturedDefaultProp.boolValue = false; hasCapturedDefaultProp.boolValue = false;
capturedPropertyProp.enumValueIndex = (int)UXBindingProperty.GameObjectActive; capturedPropertyProp.enumValueIndex = (int)property;
ApplyDefaultFallbackForProperty(entryProp, UXBindingProperty.GameObjectActive); ApplyDefaultFallbackForProperty(entryProp, property);
if (binding.Controller != null && binding.Controller.Controllers.Count > 0)
{
controllerIdProp.stringValue = binding.Controller.Controllers[0].Id;
}
_foldouts[index] = true; _foldouts[index] = true;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(binding);
}
private void ShowAddRuleMenu(UXBinding binding, Rect activatorRect)
{
if (binding.Controller == null || binding.Controller.Controllers.Count == 0)
{
EditorUtility.DisplayDialog("Add UX Binding Rule", "Assign a UXController before adding rules.", "OK");
return;
}
UXBindingPropertyUtility.GetSupportedProperties(binding.gameObject, _supportedProperties);
_addRuleOptions.Clear();
for (int controllerIndex = 0; controllerIndex < binding.Controller.Controllers.Count; controllerIndex++)
{
UXController.ControllerDefinition controller = binding.Controller.Controllers[controllerIndex];
if (controller == null)
{
continue;
}
for (int propertyIndex = 0; propertyIndex < _supportedProperties.Count; propertyIndex++)
{
UXBindingProperty property = _supportedProperties[propertyIndex];
if (HasRule(controller.Id, property))
{
continue;
}
UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(property);
_addRuleOptions.Add(new AddRuleOption(controller.Id, controller.Name, property, metadata.DisplayName));
}
}
if (_addRuleOptions.Count == 0)
{
EditorUtility.DisplayDialog("Add UX Binding Rule", "All supported states already exist.", "OK");
return;
}
PopupWindow.Show(activatorRect, new AddRulePopup(this, binding, _addRuleOptions, binding.Controller.Controllers.Count > 1));
} }
private static void ResetValue(SerializedProperty valueProp) private static void ResetValue(SerializedProperty valueProp)
@ -367,6 +534,15 @@ namespace UnityEngine.UI
return; return;
} }
if (!EditorUtility.DisplayDialog(
"Delete UX Binding Rule",
$"Delete binding rule {index}? This cannot be undone outside Unity Undo.",
"Delete",
"Cancel"))
{
return;
}
Undo.RecordObject(binding, "Delete UX Binding Rule"); Undo.RecordObject(binding, "Delete UX Binding Rule");
_entriesProp.DeleteArrayElementAtIndex(index); _entriesProp.DeleteArrayElementAtIndex(index);
CleanupFoldouts(index); CleanupFoldouts(index);
@ -375,6 +551,22 @@ namespace UnityEngine.UI
GUIUtility.ExitGUI(); GUIUtility.ExitGUI();
} }
private void MoveEntry(int fromIndex, int toIndex)
{
if (fromIndex < 0 || toIndex < 0 || fromIndex >= _entriesProp.arraySize || toIndex >= _entriesProp.arraySize)
{
return;
}
_entriesProp.MoveArrayElement(fromIndex, toIndex);
bool fromExpanded = _foldouts.ContainsKey(fromIndex) && _foldouts[fromIndex];
bool toExpanded = _foldouts.ContainsKey(toIndex) && _foldouts[toIndex];
_foldouts[fromIndex] = toExpanded;
_foldouts[toIndex] = fromExpanded;
serializedObject.ApplyModifiedProperties();
GUIUtility.ExitGUI();
}
private void CleanupFoldouts(int removedIndex) private void CleanupFoldouts(int removedIndex)
{ {
_foldouts.Remove(removedIndex); _foldouts.Remove(removedIndex);
@ -411,6 +603,230 @@ namespace UnityEngine.UI
ResetValue(fallbackValueProp); ResetValue(fallbackValueProp);
} }
} }
private void AutoBindController(UXBinding binding)
{
UXController controller = binding.GetComponentInParent<UXController>();
if (controller == null)
{
EditorUtility.DisplayDialog("Auto Bind UX Controller", "No UXController found in parents.", "OK");
return;
}
Undo.RecordObject(binding, "Auto Bind UX Controller");
binding.SetController(controller);
_controllerProp.objectReferenceValue = controller;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(binding);
}
private void EnsureStyles()
{
if (_pillOn != null)
{
return;
}
_pillOn = new GUIStyle(EditorStyles.miniButton)
{
fontStyle = FontStyle.Bold,
fixedHeight = 18f,
margin = new RectOffset(1, 1, 1, 1),
padding = new RectOffset(2, 2, 1, 1)
};
_pillOff = new GUIStyle(EditorStyles.miniButton)
{
fixedHeight = 18f,
margin = new RectOffset(1, 1, 1, 1),
padding = new RectOffset(2, 2, 1, 1)
};
}
private bool DrawIndexMask(SerializedProperty maskProp, SerializedProperty indexProp, int length, bool multiSelect)
{
EditorGUILayout.LabelField(multiSelect ? "Active" : "Index", EditorStyles.miniLabel, GUILayout.Width(38f));
int mask = maskProp.intValue;
int originalMask = mask;
for (int i = 0; i < length; i++)
{
int bit = UXBinding.BindingEntry.IndexToMask(i);
bool selected = (mask & bit) != 0;
bool nextSelected = GUILayout.Toggle(selected, i.ToString(), selected ? _pillOn : _pillOff, GUILayout.Width(26f));
if (nextSelected != selected)
{
if (multiSelect)
{
if (nextSelected)
{
mask |= bit;
}
else
{
mask &= ~bit;
}
}
else
{
mask = bit;
}
}
}
if (mask == 0)
{
mask = UXBinding.BindingEntry.IndexToMask(Mathf.Clamp(indexProp.intValue, 0, length - 1));
}
maskProp.intValue = ClampMask(mask, length, indexProp.intValue);
indexProp.intValue = GetFirstSelectedIndex(maskProp.intValue);
return maskProp.intValue != originalMask;
}
private static SerializedProperty GetIndexedValueProperty(SerializedProperty indexedValuesProp, SerializedProperty fallbackValueProp, int selectedIndex)
{
for (int i = 0; i < indexedValuesProp.arraySize; i++)
{
SerializedProperty indexedValueProp = indexedValuesProp.GetArrayElementAtIndex(i);
if (indexedValueProp.FindPropertyRelative("_index").intValue == selectedIndex)
{
return indexedValueProp.FindPropertyRelative("_value");
}
}
int nextIndex = indexedValuesProp.arraySize;
indexedValuesProp.InsertArrayElementAtIndex(nextIndex);
SerializedProperty nextValueProp = indexedValuesProp.GetArrayElementAtIndex(nextIndex);
nextValueProp.FindPropertyRelative("_index").intValue = selectedIndex;
CopyValue(fallbackValueProp, nextValueProp.FindPropertyRelative("_value"));
return nextValueProp.FindPropertyRelative("_value");
}
private static void CopyValue(SerializedProperty source, SerializedProperty destination)
{
destination.FindPropertyRelative("_boolValue").boolValue = source.FindPropertyRelative("_boolValue").boolValue;
destination.FindPropertyRelative("_floatValue").floatValue = source.FindPropertyRelative("_floatValue").floatValue;
destination.FindPropertyRelative("_stringValue").stringValue = source.FindPropertyRelative("_stringValue").stringValue;
destination.FindPropertyRelative("_colorValue").colorValue = source.FindPropertyRelative("_colorValue").colorValue;
destination.FindPropertyRelative("_vector2Value").vector2Value = source.FindPropertyRelative("_vector2Value").vector2Value;
destination.FindPropertyRelative("_vector3Value").vector3Value = source.FindPropertyRelative("_vector3Value").vector3Value;
destination.FindPropertyRelative("_objectValue").objectReferenceValue = source.FindPropertyRelative("_objectValue").objectReferenceValue;
}
private static void DrawGameObjectActiveHint(int mask)
{
EditorGUILayout.LabelField($"Visible: {BuildIndexLabel(mask)} Hidden: others", EditorStyles.miniLabel);
}
private static void ForceGameObjectActiveValues(SerializedProperty valueProp, SerializedProperty fallbackModeProp, SerializedProperty fallbackValueProp)
{
valueProp.FindPropertyRelative("_boolValue").boolValue = true;
fallbackModeProp.enumValueIndex = (int)UXBindingFallbackMode.UseCustomValue;
fallbackValueProp.FindPropertyRelative("_boolValue").boolValue = false;
}
private bool HasRule(string controllerId, UXBindingProperty property)
{
for (int i = 0; i < _entriesProp.arraySize; i++)
{
SerializedProperty entry = _entriesProp.GetArrayElementAtIndex(i);
if (entry.FindPropertyRelative("_controllerId").stringValue == controllerId &&
entry.FindPropertyRelative("_property").enumValueIndex == (int)property)
{
return true;
}
}
return false;
}
private void SetAllFoldouts(bool expanded)
{
for (int i = 0; i < _entriesProp.arraySize; i++)
{
_foldouts[i] = expanded;
}
}
private static bool TryGetControllerName(UXController controller, string controllerId, out string controllerName)
{
controllerName = string.Empty;
if (controller == null || string.IsNullOrEmpty(controllerId))
{
return false;
}
for (int i = 0; i < controller.Controllers.Count; i++)
{
UXController.ControllerDefinition definition = controller.Controllers[i];
if (definition != null && definition.Id == controllerId)
{
controllerName = definition.Name;
return true;
}
}
return false;
}
private static string BuildIndexLabel(int mask)
{
if (mask == 0)
{
return "0";
}
string label = string.Empty;
for (int i = 0; i < 31; i++)
{
if ((mask & UXBinding.BindingEntry.IndexToMask(i)) == 0)
{
continue;
}
label = string.IsNullOrEmpty(label) ? i.ToString() : $"{label},{i}";
}
return label;
}
private static int ClampMask(int mask, int length, int fallbackIndex)
{
int validMask = 0;
int max = Mathf.Min(length, 31);
for (int i = 0; i < max; i++)
{
validMask |= UXBinding.BindingEntry.IndexToMask(i);
}
mask &= validMask;
if (mask == 0)
{
mask = UXBinding.BindingEntry.IndexToMask(Mathf.Clamp(fallbackIndex, 0, max - 1));
}
return mask;
}
private static int GetFirstSelectedIndex(int mask)
{
for (int i = 0; i < 31; i++)
{
if ((mask & UXBinding.BindingEntry.IndexToMask(i)) != 0)
{
return i;
}
}
return 0;
}
private static void EnsureStringArray(ref string[] array, int length)
{
if (array.Length != length)
{
array = new string[length];
}
}
} }
} }
#endif #endif

View File

@ -77,6 +77,7 @@ namespace UnityEngine.UI
SerializedProperty idProp = entryProp.FindPropertyRelative("_id"); SerializedProperty idProp = entryProp.FindPropertyRelative("_id");
SerializedProperty nameProp = entryProp.FindPropertyRelative("_name"); SerializedProperty nameProp = entryProp.FindPropertyRelative("_name");
SerializedProperty lengthProp = entryProp.FindPropertyRelative("_length"); SerializedProperty lengthProp = entryProp.FindPropertyRelative("_length");
SerializedProperty defaultIndexProp = entryProp.FindPropertyRelative("_defaultIndex");
SerializedProperty descriptionProp = entryProp.FindPropertyRelative("_description"); SerializedProperty descriptionProp = entryProp.FindPropertyRelative("_description");
bool expanded = _foldouts.ContainsKey(index) && _foldouts[index]; bool expanded = _foldouts.ContainsKey(index) && _foldouts[index];
@ -110,6 +111,7 @@ namespace UnityEngine.UI
{ {
EditorGUILayout.PropertyField(nameProp, new GUIContent("Name")); EditorGUILayout.PropertyField(nameProp, new GUIContent("Name"));
lengthProp.intValue = Mathf.Max(1, EditorGUILayout.IntField("Length", Mathf.Max(1, lengthProp.intValue))); lengthProp.intValue = Mathf.Max(1, EditorGUILayout.IntField("Length", Mathf.Max(1, lengthProp.intValue)));
defaultIndexProp.intValue = Mathf.Clamp(EditorGUILayout.IntField("Default Index", defaultIndexProp.intValue), 0, lengthProp.intValue - 1);
EditorGUILayout.PropertyField(descriptionProp, new GUIContent("Description")); EditorGUILayout.PropertyField(descriptionProp, new GUIContent("Description"));
EditorGUI.BeginDisabledGroup(true); EditorGUI.BeginDisabledGroup(true);
@ -181,6 +183,7 @@ namespace UnityEngine.UI
entryProp.FindPropertyRelative("_id").stringValue = string.Empty; entryProp.FindPropertyRelative("_id").stringValue = string.Empty;
entryProp.FindPropertyRelative("_name").stringValue = $"Controller {index + 1}"; entryProp.FindPropertyRelative("_name").stringValue = $"Controller {index + 1}";
entryProp.FindPropertyRelative("_length").intValue = 2; entryProp.FindPropertyRelative("_length").intValue = 2;
entryProp.FindPropertyRelative("_defaultIndex").intValue = 0;
entryProp.FindPropertyRelative("_description").stringValue = string.Empty; entryProp.FindPropertyRelative("_description").stringValue = string.Empty;
_foldouts[index] = true; _foldouts[index] = true;
@ -195,6 +198,15 @@ namespace UnityEngine.UI
SerializedProperty entryProp = _controllersProp.GetArrayElementAtIndex(index); SerializedProperty entryProp = _controllersProp.GetArrayElementAtIndex(index);
string deletedControllerId = entryProp.FindPropertyRelative("_id").stringValue; string deletedControllerId = entryProp.FindPropertyRelative("_id").stringValue;
int referenceCount = CountBindingReferences(controller, deletedControllerId);
if (referenceCount > 0 && !EditorUtility.DisplayDialog(
"Delete UX Controller",
$"This controller is referenced by {referenceCount} binding rule(s). Delete and clear those references?",
"Delete",
"Cancel"))
{
return;
}
Undo.RecordObject(controller, "Delete UX Controller"); Undo.RecordObject(controller, "Delete UX Controller");
_controllersProp.DeleteArrayElementAtIndex(index); _controllersProp.DeleteArrayElementAtIndex(index);
@ -261,6 +273,37 @@ namespace UnityEngine.UI
} }
} }
} }
private static int CountBindingReferences(UXController controller, string controllerId)
{
if (controller == null || string.IsNullOrEmpty(controllerId))
{
return 0;
}
int count = 0;
IReadOnlyList<UXBinding> bindings = controller.Bindings;
for (int i = 0; i < bindings.Count; i++)
{
UXBinding binding = bindings[i];
if (binding == null)
{
continue;
}
IReadOnlyList<UXBinding.BindingEntry> entries = binding.Entries;
for (int entryIndex = 0; entryIndex < entries.Count; entryIndex++)
{
UXBinding.BindingEntry entry = entries[entryIndex];
if (entry != null && entry.ControllerId == controllerId)
{
count++;
}
}
}
return count;
}
} }
} }
#endif #endif

View File

@ -8,29 +8,39 @@ namespace UnityEngine.UI
public sealed class UXControllerSceneOverlayWindow : EditorWindow public sealed class UXControllerSceneOverlayWindow : EditorWindow
{ {
private const string OverlayEnabledKey = "AlicizaX.UI.UXControllerSceneOverlay.Enabled"; private const string OverlayEnabledKey = "AlicizaX.UI.UXControllerSceneOverlay.Enabled";
private const string OverlayAutoFocusKey = "AlicizaX.UI.UXControllerSceneOverlay.AutoFocus"; private const string MenuPath = "Window/UX/Controller Scene Overlay";
private const float OverlayWidth = 360f; private const float OverlayWidth = 340f;
private const float OverlayMargin = 12f; private const float OverlayMargin = 10f;
private static bool s_overlayEnabled; private static bool s_overlayEnabled;
private static bool s_autoFocusSceneView;
private static Vector2 s_scrollPosition; private static Vector2 s_scrollPosition;
private static bool s_registered; private static bool s_registered;
private static readonly List<UXController> Controllers = new List<UXController>();
private static GUIStyle s_indexOnStyle;
private static GUIStyle s_indexOffStyle;
[MenuItem("Window/UX/Controller Scene Overlay")] [MenuItem(MenuPath)]
public static void ShowWindow() public static void ToggleOverlay()
{ {
var window = GetWindow<UXControllerSceneOverlayWindow>("UX Controller Overlay"); s_overlayEnabled = !s_overlayEnabled;
window.minSize = new Vector2(320f, 140f); EditorPrefs.SetBool(OverlayEnabledKey, s_overlayEnabled);
window.Show(); Menu.SetChecked(MenuPath, s_overlayEnabled);
EnsureRegistered(); EnsureRegistered();
SceneView.RepaintAll();
}
[MenuItem(MenuPath, true)]
private static bool ToggleOverlayValidate()
{
Menu.SetChecked(MenuPath, s_overlayEnabled);
return true;
} }
[InitializeOnLoadMethod] [InitializeOnLoadMethod]
private static void Initialize() private static void Initialize()
{ {
s_overlayEnabled = EditorPrefs.GetBool(OverlayEnabledKey, false); s_overlayEnabled = EditorPrefs.GetBool(OverlayEnabledKey, false);
s_autoFocusSceneView = EditorPrefs.GetBool(OverlayAutoFocusKey, false); Menu.SetChecked(MenuPath, s_overlayEnabled);
EnsureRegistered(); EnsureRegistered();
} }
@ -45,37 +55,6 @@ namespace UnityEngine.UI
s_registered = true; s_registered = true;
} }
private void OnGUI()
{
EditorGUILayout.LabelField("Scene View Overlay", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"When enabled, the Scene View will display all UXController components in the current loaded scenes and let you preview them directly.",
MessageType.Info);
EditorGUI.BeginChangeCheck();
bool overlayEnabled = EditorGUILayout.Toggle("Enable Overlay", s_overlayEnabled);
bool autoFocus = EditorGUILayout.Toggle("Focus SceneView On Open", s_autoFocusSceneView);
if (EditorGUI.EndChangeCheck())
{
s_overlayEnabled = overlayEnabled;
s_autoFocusSceneView = autoFocus;
EditorPrefs.SetBool(OverlayEnabledKey, s_overlayEnabled);
EditorPrefs.SetBool(OverlayAutoFocusKey, s_autoFocusSceneView);
SceneView.RepaintAll();
}
EditorGUILayout.Space(8f);
if (GUILayout.Button("Repaint Scene View"))
{
SceneView.RepaintAll();
}
if (GUILayout.Button("Open Scene View"))
{
SceneView.FocusWindowIfItsOpen<SceneView>();
}
}
private static void OnSceneGui(SceneView sceneView) private static void OnSceneGui(SceneView sceneView)
{ {
if (!s_overlayEnabled) if (!s_overlayEnabled)
@ -83,51 +62,44 @@ namespace UnityEngine.UI
return; return;
} }
if (s_autoFocusSceneView && sceneView != null) GetSceneControllers(Controllers);
{
s_autoFocusSceneView = false;
EditorPrefs.SetBool(OverlayAutoFocusKey, false);
sceneView.Focus();
}
List<UXController> controllers = GetSceneControllers();
Handles.BeginGUI(); Handles.BeginGUI();
float height = Mathf.Max(160f, sceneView.position.height - OverlayMargin * 2f); float height = Mathf.Min(Mathf.Max(180f, sceneView.position.height - OverlayMargin * 2f), 520f);
Rect area = new Rect( Rect area = new Rect(
sceneView.position.width - OverlayWidth - OverlayMargin, sceneView.position.width - OverlayWidth - OverlayMargin,
OverlayMargin, OverlayMargin,
OverlayWidth, OverlayWidth,
height); height);
GUILayout.BeginArea(area, EditorStyles.helpBox); GUILayout.BeginArea(area, GUI.skin.window);
DrawOverlayContent(controllers); DrawOverlayContent(Controllers);
GUILayout.EndArea(); GUILayout.EndArea();
Handles.EndGUI(); Handles.EndGUI();
} }
private static void DrawOverlayContent(List<UXController> controllers) private static void DrawOverlayContent(List<UXController> controllers)
{ {
EnsureStyles();
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("UX Controllers", EditorStyles.boldLabel); EditorGUILayout.LabelField("UX Controller Preview", EditorStyles.boldLabel);
EditorGUILayout.LabelField(controllers.Count.ToString(), EditorStyles.miniBoldLabel, GUILayout.Width(24f));
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
if (GUILayout.Button("Hide", GUILayout.Width(64f))) if (GUILayout.Button("X", EditorStyles.miniButton, GUILayout.Width(22f)))
{ {
s_overlayEnabled = false; s_overlayEnabled = false;
EditorPrefs.SetBool(OverlayEnabledKey, s_overlayEnabled); EditorPrefs.SetBool(OverlayEnabledKey, s_overlayEnabled);
SceneView.RepaintAll(); Menu.SetChecked(MenuPath, false);
}
if (GUILayout.Button("Refresh", GUILayout.Width(64f)))
{
SceneView.RepaintAll(); SceneView.RepaintAll();
} }
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4f); EditorGUILayout.Space(2f);
if (controllers.Count == 0) if (controllers.Count == 0)
{ {
EditorGUILayout.HelpBox("No UXController found in the current loaded scenes.", MessageType.Info); EditorGUILayout.HelpBox("No UXController in loaded scenes.", MessageType.Info);
return; return;
} }
@ -152,16 +124,17 @@ namespace UnityEngine.UI
EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"{index + 1}. {controller.gameObject.name}", EditorStyles.boldLabel); EditorGUILayout.LabelField(controller.gameObject.name, EditorStyles.boldLabel);
EditorGUILayout.LabelField($"C:{controller.ControllerCount} B:{controller.Bindings.Count}", EditorStyles.miniLabel, GUILayout.Width(72f));
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
if (GUILayout.Button("Select", GUILayout.Width(52f))) if (GUILayout.Button("Ping", EditorStyles.miniButtonLeft, GUILayout.Width(42f)))
{ {
Selection.activeGameObject = controller.gameObject; Selection.activeGameObject = controller.gameObject;
EditorGUIUtility.PingObject(controller.gameObject); EditorGUIUtility.PingObject(controller.gameObject);
} }
if (GUILayout.Button("Reset", GUILayout.Width(52f))) if (GUILayout.Button("Reset", EditorStyles.miniButtonRight, GUILayout.Width(44f)))
{ {
controller.ResetAllControllers(); controller.ResetAllControllers();
EditorUtility.SetDirty(controller); EditorUtility.SetDirty(controller);
@ -170,7 +143,6 @@ namespace UnityEngine.UI
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
EditorGUILayout.LabelField(GetHierarchyPath(controller.transform), EditorStyles.miniLabel); EditorGUILayout.LabelField(GetHierarchyPath(controller.transform), EditorStyles.miniLabel);
EditorGUILayout.LabelField($"Bindings: {controller.Bindings.Count}", EditorStyles.miniLabel);
for (int controllerIndex = 0; controllerIndex < controller.ControllerCount; controllerIndex++) for (int controllerIndex = 0; controllerIndex < controller.ControllerCount; controllerIndex++)
{ {
@ -188,38 +160,33 @@ namespace UnityEngine.UI
private static void DrawDefinitionPreview(UXController controller, UXController.ControllerDefinition definition) private static void DrawDefinitionPreview(UXController controller, UXController.ControllerDefinition definition)
{ {
EditorGUILayout.Space(3f); int currentIndex = Mathf.Max(0, definition.SelectedIndex);
EditorGUILayout.BeginVertical(EditorStyles.helpBox); int length = Mathf.Max(1, definition.Length);
EditorGUILayout.LabelField(definition.Name, EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(definition.Name, EditorStyles.miniBoldLabel, GUILayout.Width(96f));
for (int i = 0; i < length; i++)
{
bool selected = i == currentIndex;
GUIStyle style = selected ? s_indexOnStyle : s_indexOffStyle;
if (GUILayout.Toggle(selected, i.ToString(), style, GUILayout.Width(26f)) != selected)
{
controller.SetControllerIndex(definition.Id, i);
EditorUtility.SetDirty(controller);
SceneView.RepaintAll();
}
}
EditorGUILayout.EndHorizontal();
if (!string.IsNullOrWhiteSpace(definition.Description)) if (!string.IsNullOrWhiteSpace(definition.Description))
{ {
EditorGUILayout.LabelField(definition.Description, EditorStyles.miniLabel); EditorGUILayout.LabelField(definition.Description, EditorStyles.miniLabel);
} }
int currentIndex = Mathf.Max(0, definition.SelectedIndex);
int length = Mathf.Max(1, definition.Length);
string[] options = new string[length];
for (int i = 0; i < length; i++)
{
options[i] = i.ToString();
}
EditorGUI.BeginChangeCheck();
int newIndex = GUILayout.SelectionGrid(currentIndex, options, Mathf.Min(length, 5));
if (EditorGUI.EndChangeCheck())
{
controller.SetControllerIndex(definition.Id, newIndex);
EditorUtility.SetDirty(controller);
SceneView.RepaintAll();
}
EditorGUILayout.EndVertical();
} }
private static List<UXController> GetSceneControllers() private static void GetSceneControllers(List<UXController> results)
{ {
var results = new List<UXController>(); results.Clear();
UXController[] allControllers = Resources.FindObjectsOfTypeAll<UXController>(); UXController[] allControllers = Resources.FindObjectsOfTypeAll<UXController>();
for (int i = 0; i < allControllers.Length; i++) for (int i = 0; i < allControllers.Length; i++)
@ -248,8 +215,35 @@ namespace UnityEngine.UI
results.Add(controller); results.Add(controller);
} }
results.Sort((left, right) => string.CompareOrdinal(GetHierarchyPath(left.transform), GetHierarchyPath(right.transform))); results.Sort(CompareControllers);
return results; }
private static int CompareControllers(UXController left, UXController right)
{
return string.CompareOrdinal(GetHierarchyPath(left.transform), GetHierarchyPath(right.transform));
}
private static void EnsureStyles()
{
if (s_indexOnStyle != null)
{
return;
}
s_indexOnStyle = new GUIStyle(EditorStyles.miniButton)
{
fontStyle = FontStyle.Bold,
fixedHeight = 18f,
margin = new RectOffset(1, 1, 1, 1),
padding = new RectOffset(2, 2, 1, 1)
};
s_indexOffStyle = new GUIStyle(EditorStyles.miniButton)
{
fixedHeight = 18f,
margin = new RectOffset(1, 1, 1, 1),
padding = new RectOffset(2, 2, 1, 1)
};
} }
private static string GetHierarchyPath(Transform target) private static string GetHierarchyPath(Transform target)

View File

@ -1,8 +1,7 @@
using System.Collections.Generic; using UnityEditor;
using UnityEditorInternal; using UnityEditorInternal;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using UnityEditor;
namespace UnityEditor.UI namespace UnityEditor.UI
{ {
@ -12,23 +11,23 @@ namespace UnityEditor.UI
private SerializedProperty m_Toggles; private SerializedProperty m_Toggles;
private SerializedProperty m_AllowSwitchOff; private SerializedProperty m_AllowSwitchOff;
private SerializedProperty m_DefaultToggle; private SerializedProperty m_DefaultToggle;
private UXGroup _target; private UXGroup m_Target;
private ReorderableList _reorderableList; private ReorderableList m_ReorderableList;
private void OnEnable() private void OnEnable()
{ {
_target = (UXGroup)target; m_Target = (UXGroup)target;
m_Toggles = serializedObject.FindProperty("m_Toggles"); m_Toggles = serializedObject.FindProperty("m_Toggles");
m_AllowSwitchOff = serializedObject.FindProperty("m_AllowSwitchOff"); m_AllowSwitchOff = serializedObject.FindProperty("m_AllowSwitchOff");
m_DefaultToggle = serializedObject.FindProperty("m_DefaultToggle"); m_DefaultToggle = serializedObject.FindProperty("m_DefaultToggle");
_reorderableList = new ReorderableList(serializedObject, m_Toggles, true, true, true, true) m_ReorderableList = new ReorderableList(serializedObject, m_Toggles, true, true, true, true)
{ {
drawHeaderCallback = DrawHeader, drawHeaderCallback = DrawHeader,
drawElementCallback = DrawElement, drawElementCallback = DrawElement,
onAddCallback = OnAddList,
onRemoveCallback = OnRemoveList, onRemoveCallback = OnRemoveList,
onChangedCallback = OnChanged, onChangedCallback = OnChanged
displayAdd = false,
}; };
} }
@ -37,160 +36,166 @@ namespace UnityEditor.UI
serializedObject.Update(); serializedObject.Update();
EditorGUILayout.PropertyField(m_AllowSwitchOff); EditorGUILayout.PropertyField(m_AllowSwitchOff);
// Default selector: only show toggles that are currently in the group's m_Toggles list
DrawDefaultToggleSelector(); DrawDefaultToggleSelector();
DrawTools();
bool isPlaying = Application.isPlaying || EditorApplication.isPlaying; bool isPlaying = Application.isPlaying || EditorApplication.isPlaying;
m_ReorderableList.draggable = !isPlaying;
m_ReorderableList.displayAdd = !isPlaying;
m_ReorderableList.displayRemove = !isPlaying;
_reorderableList.draggable = !isPlaying; bool previousEnabled = GUI.enabled;
_reorderableList.displayAdd = !isPlaying; GUI.enabled = !isPlaying;
_reorderableList.displayRemove = !isPlaying; m_ReorderableList.DoLayoutList();
GUI.enabled = previousEnabled;
bool prevEnabled = GUI.enabled;
if (isPlaying) GUI.enabled = false;
_reorderableList.DoLayoutList();
GUI.enabled = prevEnabled;
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
// 在编辑器下尽量实时同步状态
if (!Application.isPlaying) if (!Application.isPlaying)
{ m_Target.EnsureValidState();
// 保证序列化数据写入后同步 group 状态
_target.EnsureValidState();
}
} }
private void DrawDefaultToggleSelector() private void DrawDefaultToggleSelector()
{ {
// Build a list of current toggles (non-null) int toggleCount = CountValidToggles();
List<UXToggle> toggles = new List<UXToggle>(); bool requireDefault = !m_AllowSwitchOff.boolValue && toggleCount > 0;
int optionOffset = requireDefault ? 0 : 1;
string[] options = new string[toggleCount + optionOffset];
UXToggle[] toggles = new UXToggle[toggleCount];
if (!requireDefault)
options[0] = "<None>";
int write = 0;
for (int i = 0; i < m_Toggles.arraySize; i++) for (int i = 0; i < m_Toggles.arraySize; i++)
{ {
var elem = m_Toggles.GetArrayElementAtIndex(i); UXToggle toggle = m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue as UXToggle;
var t = elem.objectReferenceValue as UXToggle; if (toggle == null)
if (t != null) continue;
toggles.Add(t);
toggles[write] = toggle;
options[write + optionOffset] = "[" + i + "] " + toggle.name;
write++;
} }
// 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; UXToggle currentDefault = m_DefaultToggle.objectReferenceValue as UXToggle;
int currentIndex = 0; int currentIndex = requireDefault ? -1 : 0;
if (currentDefault != null) for (int i = 0; i < toggles.Length; i++)
{ {
int found = toggles.IndexOf(currentDefault); if (toggles[i] == currentDefault)
if (found >= 0)
currentIndex = found + 1; // +1 because 0 is <None>
else
{ {
// Current default is not in the list -> clear it currentIndex = i + optionOffset;
m_DefaultToggle.objectReferenceValue = null; break;
serializedObject.ApplyModifiedProperties();
currentIndex = 0;
} }
} }
if (requireDefault && currentIndex < 0)
{
currentDefault = GetSelectedToggle(toggles);
if (currentDefault == null)
currentDefault = toggles[0];
m_DefaultToggle.objectReferenceValue = currentDefault;
for (int i = 0; i < toggles.Length; i++)
{
if (toggles[i] == currentDefault)
{
currentIndex = i;
break;
}
}
}
else if (!requireDefault && currentDefault != null && currentIndex == 0)
{
m_DefaultToggle.objectReferenceValue = null;
}
EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup("Default Toggle", currentIndex, options); int newIndex = EditorGUILayout.Popup("Default Toggle", currentIndex, options);
if (EditorGUI.EndChangeCheck()) if (EditorGUI.EndChangeCheck())
{ {
UXToggle newDefault = null; UXToggle newDefault = newIndex >= optionOffset ? toggles[newIndex - optionOffset] : null;
if (newIndex > 0)
newDefault = toggles[newIndex - 1];
m_DefaultToggle.objectReferenceValue = newDefault; m_DefaultToggle.objectReferenceValue = newDefault;
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
// 如果选择了一个非空默认项,则在 allowSwitchOff == false 时将其设为选中状态(并通知组)
if (newDefault != null) if (newDefault != null)
{ {
// 确保该 toggle 在 group 中(理论上应该如此) AssignGroup(newDefault, m_Target);
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 static UXToggle GetSelectedToggle(UXToggle[] toggles)
{
for (int i = 0; i < toggles.Length; i++)
{
UXToggle toggle = toggles[i];
if (toggle != null && toggle.isOn)
return toggle;
}
return null;
}
private void DrawTools()
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Collect Children"))
{
CollectChildren();
}
if (GUILayout.Button("Clean Nulls"))
{
CleanNulls();
}
if (GUILayout.Button("Sort By Hierarchy"))
{
SortByHierarchy();
}
EditorGUILayout.EndHorizontal();
}
private void DrawHeader(Rect rect) private void DrawHeader(Rect rect)
{ {
EditorGUI.LabelField(rect, "Toggles", EditorStyles.boldLabel); EditorGUI.LabelField(rect, "Toggles", EditorStyles.boldLabel);
} }
// 记录旧的引用用于侦测变化
private UXToggle previousRef;
private void DrawElement(Rect rect, int index, bool isActive, bool isFocused) private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
{ {
SerializedProperty element = m_Toggles.GetArrayElementAtIndex(index); SerializedProperty element = m_Toggles.GetArrayElementAtIndex(index);
UXToggle oldToggle = element.objectReferenceValue as UXToggle;
rect.y += 2; rect.y += 2;
Rect fieldRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight); Rect fieldRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight);
string label = oldToggle != null ? "[" + index + "] " + oldToggle.name : "[" + index + "] Null";
UXToggle oldButton = element.objectReferenceValue as UXToggle; bool duplicate = oldToggle != null && HasDuplicate(oldToggle, index);
bool wrongGroup = oldToggle != null && oldToggle.group != null && oldToggle.group != m_Target;
string label = $"[{index}] {(oldButton != null ? oldButton.name : "Null")}"; if (duplicate || wrongGroup)
EditorGUI.DrawRect(fieldRect, new Color(1f, 0.55f, 0f, 0.2f));
EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck();
UXToggle newToggle = EditorGUI.ObjectField(fieldRect, label, oldToggle, typeof(UXToggle), true) as UXToggle;
var newRef = EditorGUI.ObjectField(fieldRect, label, oldButton, typeof(UXToggle), true) as UXToggle;
if (EditorGUI.EndChangeCheck()) if (EditorGUI.EndChangeCheck())
{ {
// 先处理 Remove旧值存在且不同 if (oldToggle != null && oldToggle != newToggle)
if (oldButton != null && oldButton != newRef) AssignGroup(oldToggle, null);
{
OnRemove(oldButton);
}
// 再处理 Add新值非空 if (newToggle != null && oldToggle != newToggle)
if (newRef != null && oldButton != newRef) AssignGroup(newToggle, m_Target);
{
OnAdd(newRef);
}
// 最后把引用写回去 element.objectReferenceValue = newToggle;
element.objectReferenceValue = newRef;
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
m_Target.EnsureValidState();
} }
} }
private void OnAddList(ReorderableList list) private void OnAddList(ReorderableList list)
{ {
int newIndex = m_Toggles.arraySize;
m_Toggles.arraySize++; m_Toggles.arraySize++;
serializedObject.ApplyModifiedProperties(); m_Toggles.GetArrayElementAtIndex(m_Toggles.arraySize - 1).objectReferenceValue = null;
var newElem = m_Toggles.GetArrayElementAtIndex(newIndex);
newElem.objectReferenceValue = null;
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
} }
@ -199,72 +204,122 @@ namespace UnityEditor.UI
if (list.index < 0 || list.index >= m_Toggles.arraySize) if (list.index < 0 || list.index >= m_Toggles.arraySize)
return; return;
var oldButton = m_Toggles.GetArrayElementAtIndex(list.index).objectReferenceValue as UXToggle; UXToggle oldToggle = m_Toggles.GetArrayElementAtIndex(list.index).objectReferenceValue as UXToggle;
if (oldButton) if (oldToggle != null)
{ AssignGroup(oldToggle, null);
OnRemove(oldButton);
}
m_Toggles.DeleteArrayElementAtIndex(list.index); m_Toggles.DeleteArrayElementAtIndex(list.index);
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
m_Target.EnsureValidState();
} }
private void OnChanged(ReorderableList list) private void OnChanged(ReorderableList list)
{ {
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
// 编辑器变动后同步 group 状态和默认项
if (!Application.isPlaying) if (!Application.isPlaying)
_target.EnsureValidState(); m_Target.EnsureValidState();
} }
// ======================== private void CollectChildren()
// 自动调用的新增方法
// ========================
private void OnAdd(UXToggle toggle)
{ {
if (toggle == null) UXToggle[] toggles = m_Target.GetComponentsInChildren<UXToggle>(true);
return; m_Toggles.arraySize = toggles.Length;
for (int i = 0; i < toggles.Length; i++)
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; m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue = toggles[i];
serializedObject.ApplyModifiedProperties(); AssignGroup(toggles[i], m_Target);
} }
if (!Application.isPlaying && _target != null) serializedObject.ApplyModifiedProperties();
_target.EnsureValidState(); m_Target.EnsureValidState();
EditorUtility.SetDirty(m_Target);
}
private void CleanNulls()
{
for (int i = m_Toggles.arraySize - 1; i >= 0; i--)
{
if (m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue == null)
m_Toggles.DeleteArrayElementAtIndex(i);
}
serializedObject.ApplyModifiedProperties();
m_Target.EnsureValidState();
EditorUtility.SetDirty(m_Target);
}
private void SortByHierarchy()
{
int count = CountValidToggles();
UXToggle[] toggles = new UXToggle[count];
int write = 0;
for (int i = 0; i < m_Toggles.arraySize; i++)
{
UXToggle toggle = m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue as UXToggle;
if (toggle != null)
{
toggles[write] = toggle;
write++;
}
}
System.Array.Sort(toggles, CompareHierarchyIndex);
m_Toggles.arraySize = toggles.Length;
for (int i = 0; i < toggles.Length; i++)
m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue = toggles[i];
serializedObject.ApplyModifiedProperties();
m_Target.EnsureValidState();
EditorUtility.SetDirty(m_Target);
}
private int CountValidToggles()
{
int count = 0;
for (int i = 0; i < m_Toggles.arraySize; i++)
{
if (m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue != null)
count++;
}
return count;
}
private bool HasDuplicate(UXToggle toggle, int selfIndex)
{
for (int i = 0; i < m_Toggles.arraySize; i++)
{
if (i != selfIndex && m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue == toggle)
return true;
}
return false;
}
private static int CompareHierarchyIndex(UXToggle left, UXToggle right)
{
int leftIndex = left.transform.GetSiblingIndex();
int rightIndex = right.transform.GetSiblingIndex();
if (leftIndex < rightIndex)
return -1;
if (leftIndex > rightIndex)
return 1;
return 0;
}
private static void AssignGroup(UXToggle toggle, UXGroup group)
{
if (toggle == null)
return;
SerializedObject serializedToggle = new SerializedObject(toggle);
SerializedProperty groupProperty = serializedToggle.FindProperty("m_Group");
groupProperty.objectReferenceValue = group;
serializedToggle.ApplyModifiedProperties();
toggle.group = group;
EditorUtility.SetDirty(toggle);
} }
} }
} }

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 292ca921cd4242d4be9f76bef9bfc08f guid: 292ca921cd4242d4be9f76bef9bfc08f
timeCreated: 1760343702 MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -11,10 +11,12 @@ namespace UnityEditor.UI
private SerializedProperty _hotkeyAction; private SerializedProperty _hotkeyAction;
private SerializedProperty _hotkeyPressType; private SerializedProperty _hotkeyPressType;
private SerializedProperty _component; private SerializedProperty _component;
private SerializedProperty _holder;
private void OnEnable() private void OnEnable()
{ {
_component = serializedObject.FindProperty("_component"); _component = serializedObject.FindProperty("_component");
_holder = serializedObject.FindProperty("_holder");
_hotkeyAction = serializedObject.FindProperty("_hotkeyAction"); _hotkeyAction = serializedObject.FindProperty("_hotkeyAction");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType"); _hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
} }
@ -30,7 +32,7 @@ namespace UnityEditor.UI
MessageType.Info MessageType.Info
); );
if (hotkeyComponent.GetComponentInParent<UIHolderObjectBase>(true) == null) if (_holder.objectReferenceValue == null)
{ {
EditorGUILayout.HelpBox( EditorGUILayout.HelpBox(
"No UIHolderObjectBase was found in parents. This hotkey will not register at runtime.", "No UIHolderObjectBase was found in parents. This hotkey will not register at runtime.",
@ -46,6 +48,16 @@ namespace UnityEditor.UI
_component.objectReferenceValue = submitHandler; _component.objectReferenceValue = submitHandler;
} }
} }
else if (_component.objectReferenceValue is not ISubmitHandler)
{
EditorGUILayout.HelpBox("Submit target must implement ISubmitHandler. The invalid reference will be cleared.", MessageType.Error);
_component.objectReferenceValue = null;
}
if (_hotkeyAction.objectReferenceValue == null)
{
EditorGUILayout.HelpBox("Input Action is required. This hotkey will not register at runtime.", MessageType.Error);
}
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{ {
@ -53,6 +65,7 @@ namespace UnityEditor.UI
EditorGUI.BeginDisabledGroup(true); EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.PropertyField(_component, new GUIContent("Component")); EditorGUILayout.PropertyField(_component, new GUIContent("Component"));
EditorGUILayout.PropertyField(_holder, new GUIContent("Holder"));
EditorGUI.EndDisabledGroup(); EditorGUI.EndDisabledGroup();
EditorGUILayout.PropertyField(_hotkeyAction, new GUIContent("Input Action")); EditorGUILayout.PropertyField(_hotkeyAction, new GUIContent("Input Action"));

View File

@ -4,7 +4,6 @@ using UnityEditor;
using UnityEngine; using UnityEngine;
using System.IO; using System.IO;
using UnityEditorInternal; using UnityEditorInternal;
using System.Linq;
using UnityEditor.AnimatedValues; using UnityEditor.AnimatedValues;
namespace UnityEngine.UI namespace UnityEngine.UI
@ -289,15 +288,6 @@ namespace UnityEngine.UI
if (EditorGUI.EndChangeCheck()) if (EditorGUI.EndChangeCheck())
{ {
m_ColorType.intValue = (int)type; m_ColorType.intValue = (int)type;
if ((int)type == 1 && m_Type.intValue != 0)
{
m_Type.intValue = 0;
}
if ((int)type == 1 && m_FlipMode.intValue != 0)
{
m_FlipMode.intValue = 0;
}
} }
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
@ -399,15 +389,7 @@ namespace UnityEngine.UI
protected void TypeGUI() protected void TypeGUI()
{ {
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_Type, m_SpriteTypeContent); EditorGUILayout.PropertyField(m_Type, m_SpriteTypeContent);
if (EditorGUI.EndChangeCheck())
{
if (m_Type.intValue != 0 && m_ColorType.intValue == 1)
{
m_ColorType.intValue = 0;
}
}
++EditorGUI.indentLevel; ++EditorGUI.indentLevel;
{ {
@ -415,7 +397,17 @@ namespace UnityEngine.UI
bool showSlicedOrTiled = (!m_Type.hasMultipleDifferentValues && (typeEnum == Image.Type.Sliced || typeEnum == Image.Type.Tiled)); bool showSlicedOrTiled = (!m_Type.hasMultipleDifferentValues && (typeEnum == Image.Type.Sliced || typeEnum == Image.Type.Tiled));
if (showSlicedOrTiled && targets.Length > 1) if (showSlicedOrTiled && targets.Length > 1)
showSlicedOrTiled = targets.Select(obj => obj as Image).All(img => img.hasBorder); {
for (int i = 0; i < targets.Length; i++)
{
Image targetImage = targets[i] as Image;
if (targetImage == null || !targetImage.hasBorder)
{
showSlicedOrTiled = false;
break;
}
}
}
m_ShowSlicedOrTiled.target = showSlicedOrTiled; m_ShowSlicedOrTiled.target = showSlicedOrTiled;
m_ShowSliced.target = (showSlicedOrTiled && !m_Type.hasMultipleDifferentValues && typeEnum == Image.Type.Sliced); m_ShowSliced.target = (showSlicedOrTiled && !m_Type.hasMultipleDifferentValues && typeEnum == Image.Type.Sliced);
@ -517,15 +509,7 @@ namespace UnityEngine.UI
"None", "Horizontal", "None", "Horizontal",
"Vertical", "FourCorner" "Vertical", "FourCorner"
}; };
EditorGUI.BeginChangeCheck();
m_FlipMode.intValue = EnumPopupLayoutEx(m_FlipModeContent.text, typeof(UXImage.FlipMode), m_FlipMode.intValue, labels); m_FlipMode.intValue = EnumPopupLayoutEx(m_FlipModeContent.text, typeof(UXImage.FlipMode), m_FlipMode.intValue, labels);
if (EditorGUI.EndChangeCheck())
{
if (m_FlipMode.intValue != 0 && m_ColorType.intValue == 1)
{
m_ColorType.intValue = 0;
}
}
//EditorGUILayout.PropertyField(m_FlipMode, m_FlipModeContent); //EditorGUILayout.PropertyField(m_FlipMode, m_FlipModeContent);
UXImage.FlipMode flipmodeEnum = (UXImage.FlipMode)m_FlipMode.enumValueIndex; UXImage.FlipMode flipmodeEnum = (UXImage.FlipMode)m_FlipMode.enumValueIndex;
@ -805,8 +789,12 @@ namespace UnityEngine.UI
GUILayout.Space(EditorGUIUtility.labelWidth); GUILayout.Space(EditorGUIUtility.labelWidth);
if (GUILayout.Button(m_CorrectButtonContent, EditorStyles.miniButton)) if (GUILayout.Button(m_CorrectButtonContent, EditorStyles.miniButton))
{ {
foreach (Graphic graphic in targets.Select(obj => obj as Graphic)) for (int i = 0; i < targets.Length; i++)
{ {
Graphic graphic = targets[i] as Graphic;
if (graphic == null)
continue;
Undo.RecordObject(graphic.rectTransform, "Set Native Size"); Undo.RecordObject(graphic.rectTransform, "Set Native Size");
graphic.SetNativeSize(); graphic.SetNativeSize();
EditorUtility.SetDirty(graphic); EditorUtility.SetDirty(graphic);
@ -819,10 +807,40 @@ namespace UnityEngine.UI
EditorGUILayout.EndFadeGroup(); EditorGUILayout.EndFadeGroup();
} }
private static readonly Dictionary<Type, int[]> s_EnumValues = new Dictionary<Type, int[]>();
private static readonly Dictionary<Type, string[]> s_EnumNames = new Dictionary<Type, string[]>();
private static int[] GetEnumValues(Type type)
{
if (!s_EnumValues.TryGetValue(type, out int[] values))
{
Array rawValues = Enum.GetValues(type);
values = new int[rawValues.Length];
for (int i = 0; i < rawValues.Length; i++)
{
values[i] = (int)rawValues.GetValue(i);
}
s_EnumValues.Add(type, values);
}
return values;
}
private static string[] GetEnumNames(Type type)
{
if (!s_EnumNames.TryGetValue(type, out string[] names))
{
names = Enum.GetNames(type);
s_EnumNames.Add(type, names);
}
return names;
}
public int EnumPopupLayoutEx(string label, Type type, int enumValueIndex, string[] labels) public int EnumPopupLayoutEx(string label, Type type, int enumValueIndex, string[] labels)
{ {
int[] ints = (int[])Enum.GetValues(type); int[] ints = GetEnumValues(type);
string[] strings = Enum.GetNames(type); string[] strings = GetEnumNames(type);
if (labels.Length != ints.Length) if (labels.Length != ints.Length)
{ {
return EditorGUILayout.IntPopup(label, enumValueIndex, strings, ints); return EditorGUILayout.IntPopup(label, enumValueIndex, strings, ints);

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 55d4289f6579aec4eaeeeda649282af4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,202 @@
#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<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;
}
}
}
private void BakeSelectables()
{
UXNavigationScope scope = (UXNavigationScope)target;
Selectable[] allSelectables = scope.GetComponentsInChildren<Selectable>(true);
List<Selectable> ownedSelectables = new List<Selectable>(allSelectables.Length);
for (int i = 0; i < allSelectables.Length; i++)
{
Selectable selectable = allSelectables[i];
if (selectable != null && selectable.GetComponentInParent<UXNavigationScope>(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<Selectable> selectables = new List<Selectable>(_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

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 0268df3dd46bb194fa4ae7ec48be7702 guid: 7cc921173c16d4b4ba1fcf1fcaa80479
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@ -1,7 +1,7 @@
using UnityEditor;
using UnityEditor.DrawUtils; using UnityEditor.DrawUtils;
using UnityEditor.Extensions; using UnityEditor.Extensions;
using UnityEditor.SceneManagement; using UnityEditor.SceneManagement;
using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
@ -11,11 +11,11 @@ namespace UnityEditor.UI
[CanEditMultipleObjects] [CanEditMultipleObjects]
internal class UXToggleEditor : UXSelectableEditor internal class UXToggleEditor : UXSelectableEditor
{ {
SerializedProperty m_OnValueChangedProperty; private SerializedProperty m_OnValueChangedProperty;
SerializedProperty m_TransitionProperty; private SerializedProperty m_TransitionProperty;
SerializedProperty m_GraphicProperty; private SerializedProperty m_GraphicProperty;
SerializedProperty m_GroupProperty; private SerializedProperty m_GroupProperty;
SerializedProperty m_IsOnProperty; private SerializedProperty m_IsOnProperty;
private SerializedProperty hoverAudioClip; private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip; private SerializedProperty clickAudioClip;
@ -48,10 +48,8 @@ namespace UnityEditor.UI
private void DrawEventTab() private void DrawEventTab()
{ {
EditorGUILayout.Space(); EditorGUILayout.Space();
serializedObject.Update(); serializedObject.Update();
EditorGUILayout.PropertyField(m_OnValueChangedProperty); EditorGUILayout.PropertyField(m_OnValueChangedProperty);
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
} }
@ -60,100 +58,88 @@ namespace UnityEditor.UI
GUILayoutHelper.DrawProperty(hoverAudioClip, customSkin, "Hover Sound", "Play", () => GUILayoutHelper.DrawProperty(hoverAudioClip, customSkin, "Hover Sound", "Play", () =>
{ {
if (hoverAudioClip.objectReferenceValue != null) if (hoverAudioClip.objectReferenceValue != null)
{
PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue); PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue);
}
}); });
GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () => GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () =>
{ {
if (clickAudioClip.objectReferenceValue != null) if (clickAudioClip.objectReferenceValue != null)
{
PlayAudio((AudioClip)clickAudioClip.objectReferenceValue); PlayAudio((AudioClip)clickAudioClip.objectReferenceValue);
}
}); });
} }
private void PlayAudio(AudioClip clip) private void PlayAudio(AudioClip clip)
{ {
if (clip != null) if (clip != null)
{
ExtensionHelper.PreviewAudioClip(clip); ExtensionHelper.PreviewAudioClip(clip);
}
} }
private void DrawImageTab() private void DrawImageTab()
{ {
EditorGUILayout.Space(); EditorGUILayout.Space();
serializedObject.Update(); serializedObject.Update();
UXToggle toggle = serializedObject.targetObject as UXToggle; UXToggle toggle = serializedObject.targetObject as UXToggle;
EditorGUI.BeginChangeCheck(); DrawIsOn(toggle);
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_TransitionProperty, customSkin, "Transition");
GUILayoutHelper.DrawProperty(m_GraphicProperty, customSkin, "Graphic"); GUILayoutHelper.DrawProperty(m_GraphicProperty, customSkin, "Graphic");
EditorGUI.BeginChangeCheck(); DrawGroup(toggle);
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(); EditorGUILayout.Space();
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
} }
private void DrawIsOn(UXToggle toggle)
{
EditorGUI.BeginChangeCheck();
GUILayoutHelper.DrawProperty(m_IsOnProperty, customSkin, "Is On");
if (!EditorGUI.EndChangeCheck())
return;
if (!Application.isPlaying)
EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene);
UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup;
bool newIsOn = m_IsOnProperty.boolValue;
bool oldIsOn = toggle.isOn;
if (!Application.isPlaying && group != null && !group.allowSwitchOff && oldIsOn && !newIsOn)
{
Debug.LogWarning("Cannot turn off the selected toggle because its group does not allow all toggles to be off.", toggle);
m_IsOnProperty.boolValue = true;
serializedObject.ApplyModifiedProperties();
return;
}
toggle.isOn = newIsOn;
}
private void DrawGroup(UXToggle toggle)
{
EditorGUI.BeginChangeCheck();
GUILayoutHelper.DrawProperty<UXGroup>(m_GroupProperty, customSkin, "UXGroup", OnGroupChanged);
if (!EditorGUI.EndChangeCheck())
return;
if (!Application.isPlaying)
EditorSceneManager.MarkSceneDirty(toggle.gameObject.scene);
UXGroup group = m_GroupProperty.objectReferenceValue as UXGroup;
toggle.group = group;
}
private void OnGroupChanged(UXGroup oldValue, UXGroup newValue)
{
UXToggle toggle = target as UXToggle;
if (toggle == null)
return;
if (oldValue != null)
oldValue.UnregisterToggle(toggle);
if (newValue != null)
newValue.RegisterToggle(toggle);
}
} }
} }

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 9301ee465f2c46d08b2fade637710625 guid: 9301ee465f2c46d08b2fade637710625
timeCreated: 1766136386 MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -106,6 +106,9 @@ namespace AlicizaX.UI
public void OnSelect(BaseEventData eventData) public void OnSelect(BaseEventData eventData)
{ {
#if UX_NAVIGATION #if UX_NAVIGATION
#if INPUTSYSTEM_SUPPORT
UXNavigationRuntime.NotifySelection(gameObject);
#endif
if ((flags & ItemInteractionFlags.Select) != 0) if ((flags & ItemInteractionFlags.Select) != 0)
{ {
host?.HandleSelect(eventData); host?.HandleSelect(eventData);

View File

@ -29,6 +29,9 @@ namespace AlicizaX.UI
public override void OnSelect(BaseEventData eventData) public override void OnSelect(BaseEventData eventData)
{ {
base.OnSelect(eventData); base.OnSelect(eventData);
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
UXNavigationRuntime.NotifySelection(gameObject);
#endif
TryEnter(defaultEntryDirection); TryEnter(defaultEntryDirection);
} }

View File

@ -37,6 +37,9 @@ namespace UnityEngine.UI
public override void OnSelect(BaseEventData eventData) public override void OnSelect(BaseEventData eventData)
{ {
base.OnSelect(eventData); base.OnSelect(eventData);
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
UXNavigationRuntime.NotifySelection(gameObject);
#endif
if (eventData is PointerEventData) if (eventData is PointerEventData)
return; return;
PlayAudio(hoverAudioClip); PlayAudio(hoverAudioClip);

View File

@ -115,10 +115,27 @@ namespace UnityEngine.UI
[Serializable] [Serializable]
public sealed class BindingEntry public sealed class BindingEntry
{ {
[Serializable]
public sealed class IndexedValue
{
[SerializeField] private int _index;
[SerializeField] private UXBindingValue _value = new UXBindingValue();
public int Index
{
get => Mathf.Max(0, _index);
set => _index = Mathf.Max(0, value);
}
public UXBindingValue Value => _value;
}
[SerializeField] private string _controllerId = string.Empty; [SerializeField] private string _controllerId = string.Empty;
[SerializeField] private int _controllerIndex; [SerializeField] private int _controllerIndex;
[SerializeField] private int _controllerIndexMask = 1;
[SerializeField] private UXBindingProperty _property = UXBindingProperty.GameObjectActive; [SerializeField] private UXBindingProperty _property = UXBindingProperty.GameObjectActive;
[SerializeField] private UXBindingValue _value = new UXBindingValue(); [SerializeField] private UXBindingValue _value = new UXBindingValue();
[SerializeField] private List<IndexedValue> _indexedValues = new List<IndexedValue>();
[SerializeField] private UXBindingFallbackMode _fallbackMode = UXBindingFallbackMode.RestoreCapturedDefault; [SerializeField] private UXBindingFallbackMode _fallbackMode = UXBindingFallbackMode.RestoreCapturedDefault;
[SerializeField] private UXBindingValue _fallbackValue = new UXBindingValue(); [SerializeField] private UXBindingValue _fallbackValue = new UXBindingValue();
[HideInInspector] [SerializeField] private UXBindingValue _capturedDefault = new UXBindingValue(); [HideInInspector] [SerializeField] private UXBindingValue _capturedDefault = new UXBindingValue();
@ -134,7 +151,17 @@ namespace UnityEngine.UI
public int ControllerIndex public int ControllerIndex
{ {
get => Mathf.Max(0, _controllerIndex); get => Mathf.Max(0, _controllerIndex);
set => _controllerIndex = Mathf.Max(0, value); set
{
_controllerIndex = Mathf.Max(0, value);
_controllerIndexMask = IndexToMask(_controllerIndex);
}
}
public int ControllerIndexMask
{
get => NormalizeMask(_controllerIndexMask, _controllerIndex);
set => _controllerIndexMask = value;
} }
public UXBindingProperty Property public UXBindingProperty Property
@ -144,6 +171,7 @@ namespace UnityEngine.UI
} }
public UXBindingValue Value => _value; public UXBindingValue Value => _value;
public List<IndexedValue> IndexedValues => _indexedValues;
public UXBindingFallbackMode FallbackMode public UXBindingFallbackMode FallbackMode
{ {
@ -156,6 +184,13 @@ namespace UnityEngine.UI
internal void Normalize() internal void Normalize()
{ {
_controllerIndex = Mathf.Max(0, _controllerIndex);
_controllerIndexMask = NormalizeMask(_controllerIndexMask, _controllerIndex);
if (_indexedValues.Count == 0)
{
EnsureIndexedValue(_controllerIndex).Value.CopyFrom(_value);
}
if (_property != UXBindingProperty.GameObjectActive) if (_property != UXBindingProperty.GameObjectActive)
{ {
return; return;
@ -168,36 +203,86 @@ namespace UnityEngine.UI
} }
} }
internal void CaptureDefault(GameObject target) [NonSerialized] private int _runtimeControllerSlot = -1;
[NonSerialized] private UXBindingResolvedTarget _runtimeTarget;
[NonSerialized] private bool _runtimeSupported;
internal int RuntimeControllerSlot => _runtimeControllerSlot;
internal bool RuntimeSupported => _runtimeSupported;
internal void BuildRuntime(GameObject target, UXController controller)
{ {
if (target == null || !UXBindingPropertyUtility.IsSupported(target, _property)) _runtimeControllerSlot = -1;
_runtimeSupported = UXBindingPropertyUtility.Resolve(target, _property, out _runtimeTarget);
if (controller != null && !string.IsNullOrEmpty(_controllerId))
{ {
controller.TryGetControllerSlot(_controllerId, out _runtimeControllerSlot);
}
if (!_runtimeSupported)
{
_hasCapturedDefault = false;
_capturedProperty = _property;
return;
}
if (!_hasCapturedDefault || _capturedProperty != _property)
{
CaptureDefault(in _runtimeTarget);
}
}
internal void CaptureDefault(GameObject target)
{
if (!UXBindingPropertyUtility.Resolve(target, _property, out UXBindingResolvedTarget resolvedTarget))
{
_hasCapturedDefault = false;
_capturedProperty = _property;
return;
}
CaptureDefault(in resolvedTarget);
}
private void CaptureDefault(in UXBindingResolvedTarget target)
{
if (!UXBindingPropertyUtility.CaptureValue(in target, _property, _capturedDefault))
{
_hasCapturedDefault = false;
_capturedProperty = _property;
return; return;
} }
UXBindingPropertyUtility.CaptureValue(target, _property, _capturedDefault);
_capturedProperty = _property; _capturedProperty = _property;
_hasCapturedDefault = true; _hasCapturedDefault = true;
} }
internal void CaptureCurrentAsValue(GameObject target) internal void CaptureCurrentAsValue(GameObject target)
{ {
if (target == null || !UXBindingPropertyUtility.IsSupported(target, _property)) CaptureCurrentAsValue(target, _controllerIndex);
}
internal void CaptureCurrentAsValue(GameObject target, int selectedIndex)
{
UXBindingValue value = GetMutableValue(selectedIndex);
if (!UXBindingPropertyUtility.CaptureValue(target, _property, value))
{ {
return; return;
} }
UXBindingPropertyUtility.CaptureValue(target, _property, _value); if (selectedIndex == _controllerIndex)
{
_value.CopyFrom(value);
}
} }
internal void CaptureCurrentAsFallback(GameObject target) internal void CaptureCurrentAsFallback(GameObject target)
{ {
if (target == null || !UXBindingPropertyUtility.IsSupported(target, _property)) if (!UXBindingPropertyUtility.CaptureValue(target, _property, _fallbackValue))
{ {
return; return;
} }
UXBindingPropertyUtility.CaptureValue(target, _property, _fallbackValue);
} }
internal void ResetToCapturedDefault(GameObject target) internal void ResetToCapturedDefault(GameObject target)
@ -215,26 +300,29 @@ namespace UnityEngine.UI
UXBindingPropertyUtility.ApplyValue(target, _property, _capturedDefault); UXBindingPropertyUtility.ApplyValue(target, _property, _capturedDefault);
} }
internal void Apply(GameObject target, string controllerId, int selectedIndex) internal void ApplyRuntime(int selectedIndex)
{ {
if (target == null || !string.Equals(_controllerId, controllerId, StringComparison.Ordinal)) if (!_runtimeSupported)
{ {
return; return;
} }
if (!_hasCapturedDefault || _capturedProperty != _property) if (!_hasCapturedDefault || _capturedProperty != _property)
{ {
CaptureDefault(target); CaptureDefault(in _runtimeTarget);
} }
if (!UXBindingPropertyUtility.IsSupported(target, _property)) if (_property == UXBindingProperty.GameObjectActive)
{ {
return; if (IsSelectedIndexMatched(selectedIndex))
{
UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, _value);
return;
}
} }
else if (TryGetValue(selectedIndex, out UXBindingValue indexedValue))
if (selectedIndex == _controllerIndex)
{ {
UXBindingPropertyUtility.ApplyValue(target, _property, _value); UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, indexedValue);
return; return;
} }
@ -245,14 +333,90 @@ namespace UnityEngine.UI
case UXBindingFallbackMode.RestoreCapturedDefault: case UXBindingFallbackMode.RestoreCapturedDefault:
if (_hasCapturedDefault) if (_hasCapturedDefault)
{ {
UXBindingPropertyUtility.ApplyValue(target, _property, _capturedDefault); UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, _capturedDefault);
} }
return; return;
case UXBindingFallbackMode.UseCustomValue: case UXBindingFallbackMode.UseCustomValue:
UXBindingPropertyUtility.ApplyValue(target, _property, _fallbackValue); UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, _fallbackValue);
return; return;
} }
} }
internal bool IsSelectedIndexMatched(int selectedIndex)
{
if (selectedIndex < 0 || selectedIndex >= 31)
{
return false;
}
return (ControllerIndexMask & IndexToMask(selectedIndex)) != 0;
}
internal bool TryGetValue(int selectedIndex, out UXBindingValue value)
{
for (int i = 0; i < _indexedValues.Count; i++)
{
IndexedValue indexedValue = _indexedValues[i];
if (indexedValue != null && indexedValue.Index == selectedIndex)
{
value = indexedValue.Value;
return true;
}
}
if (selectedIndex == _controllerIndex)
{
value = _value;
return true;
}
value = null;
return false;
}
internal UXBindingValue GetMutableValue(int selectedIndex)
{
return EnsureIndexedValue(selectedIndex).Value;
}
private IndexedValue EnsureIndexedValue(int selectedIndex)
{
selectedIndex = Mathf.Max(0, selectedIndex);
for (int i = 0; i < _indexedValues.Count; i++)
{
IndexedValue indexedValue = _indexedValues[i];
if (indexedValue != null && indexedValue.Index == selectedIndex)
{
return indexedValue;
}
}
IndexedValue nextValue = new IndexedValue();
nextValue.Index = selectedIndex;
nextValue.Value.CopyFrom(_value);
_indexedValues.Add(nextValue);
return nextValue;
}
internal static int IndexToMask(int index)
{
if (index < 0)
{
return 1;
}
if (index >= 31)
{
return 1 << 30;
}
return 1 << index;
}
private static int NormalizeMask(int mask, int fallbackIndex)
{
return mask != 0 ? mask : IndexToMask(fallbackIndex);
}
} }
[SerializeField] private UXController _controller; [SerializeField] private UXController _controller;
@ -261,7 +425,8 @@ namespace UnityEngine.UI
private bool _initialized; private bool _initialized;
public UXController Controller => _controller; public UXController Controller => _controller;
public List<BindingEntry> Entries => _entries; public IReadOnlyList<BindingEntry> Entries => _entries;
internal int RuntimeEntryCount => _entries.Count;
public void Initialize() public void Initialize()
{ {
@ -274,7 +439,34 @@ namespace UnityEngine.UI
NormalizeEntries(); NormalizeEntries();
EnsureControllerReference(); EnsureControllerReference();
RegisterToController(); RegisterToController();
CaptureDefaults(); BuildRuntimeEntries();
}
internal void RebuildRuntime(UXController controller)
{
_controller = controller;
NormalizeEntries();
BuildRuntimeEntries();
}
internal int GetRuntimeControllerSlot(int entryIndex)
{
return entryIndex >= 0 && entryIndex < _entries.Count && _entries[entryIndex] != null
? _entries[entryIndex].RuntimeControllerSlot
: -1;
}
internal bool IsRuntimeEntrySupported(int entryIndex)
{
return entryIndex >= 0 && entryIndex < _entries.Count && _entries[entryIndex] != null && _entries[entryIndex].RuntimeSupported;
}
internal void ApplyRuntimeEntry(int entryIndex, int selectedIndex)
{
if (entryIndex >= 0 && entryIndex < _entries.Count && _entries[entryIndex] != null)
{
_entries[entryIndex].ApplyRuntime(selectedIndex);
}
} }
public void SetController(UXController controller) public void SetController(UXController controller)
@ -291,6 +483,7 @@ namespace UnityEngine.UI
_controller = controller; _controller = controller;
RegisterToController(); RegisterToController();
BuildRuntimeEntries();
} }
public void CaptureDefaults() public void CaptureDefaults()
@ -328,7 +521,20 @@ namespace UnityEngine.UI
return; return;
} }
_controller.SetControllerIndex(entry.ControllerId, entry.ControllerIndex); _controller.SetControllerIndex(entry.ControllerId, GetFirstSelectedIndex(entry.ControllerIndexMask));
}
private static int GetFirstSelectedIndex(int mask)
{
for (int i = 0; i < 31; i++)
{
if ((mask & BindingEntry.IndexToMask(i)) != 0)
{
return i;
}
}
return 0;
} }
public void CaptureEntryValue(int entryIndex) public void CaptureEntryValue(int entryIndex)
@ -338,7 +544,28 @@ namespace UnityEngine.UI
return; return;
} }
_entries[entryIndex].CaptureCurrentAsValue(gameObject); _entries[entryIndex].CaptureCurrentAsValue(gameObject, GetFirstSelectedIndex(_entries[entryIndex].ControllerIndexMask));
}
public void CaptureEntryValue(int entryIndex, int selectedIndex)
{
if (entryIndex < 0 || entryIndex >= _entries.Count || _entries[entryIndex] == null)
{
return;
}
_entries[entryIndex].CaptureCurrentAsValue(gameObject, selectedIndex);
}
public void ApplyEntryValue(int entryIndex, int selectedIndex)
{
if (entryIndex < 0 || entryIndex >= _entries.Count || _entries[entryIndex] == null)
{
return;
}
_entries[entryIndex].BuildRuntime(gameObject, _controller);
_entries[entryIndex].ApplyRuntime(selectedIndex);
} }
public void CaptureEntryFallbackValue(int entryIndex) public void CaptureEntryFallbackValue(int entryIndex)
@ -351,18 +578,6 @@ namespace UnityEngine.UI
_entries[entryIndex].CaptureCurrentAsFallback(gameObject); _entries[entryIndex].CaptureCurrentAsFallback(gameObject);
} }
internal void OnControllerChanged(string controllerId, int selectedIndex)
{
for (int i = 0; i < _entries.Count; i++)
{
BindingEntry entry = _entries[i];
if (entry != null)
{
entry.Apply(gameObject, controllerId, selectedIndex);
}
}
}
private void Reset() private void Reset()
{ {
EnsureControllerReference(); EnsureControllerReference();
@ -405,6 +620,17 @@ namespace UnityEngine.UI
} }
} }
private void BuildRuntimeEntries()
{
for (int i = 0; i < _entries.Count; i++)
{
if (_entries[i] != null)
{
_entries[i].BuildRuntime(gameObject, _controller);
}
}
}
private void NormalizeEntries() private void NormalizeEntries()
{ {
for (int i = 0; i < _entries.Count; i++) for (int i = 0; i < _entries.Count; i++)

View File

@ -24,6 +24,18 @@ namespace UnityEngine.UI
public Type ObjectReferenceType { get; } public Type ObjectReferenceType { get; }
} }
public struct UXBindingResolvedTarget
{
public GameObject GameObject;
public Transform Transform;
public CanvasGroup CanvasGroup;
public Graphic Graphic;
public Image Image;
public Text Text;
public TextMeshProUGUI TmpText;
public RectTransform RectTransform;
}
public static class UXBindingPropertyUtility public static class UXBindingPropertyUtility
{ {
private static readonly UXBindingPropertyMetadata[] Metadata = private static readonly UXBindingPropertyMetadata[] Metadata =
@ -46,6 +58,12 @@ namespace UnityEngine.UI
public static UXBindingPropertyMetadata GetMetadata(UXBindingProperty property) public static UXBindingPropertyMetadata GetMetadata(UXBindingProperty property)
{ {
int index = (int)property;
if ((uint)index < (uint)Metadata.Length && Metadata[index].Property == property)
{
return Metadata[index];
}
for (int i = 0; i < Metadata.Length; i++) for (int i = 0; i < Metadata.Length; i++)
{ {
if (Metadata[i].Property == property) if (Metadata[i].Property == property)
@ -57,13 +75,17 @@ namespace UnityEngine.UI
return Metadata[0]; return Metadata[0];
} }
public static bool IsSupported(GameObject target, UXBindingProperty property) public static bool Resolve(GameObject target, UXBindingProperty property, out UXBindingResolvedTarget resolvedTarget)
{ {
resolvedTarget = default;
if (target == null) if (target == null)
{ {
return false; return false;
} }
resolvedTarget.GameObject = target;
resolvedTarget.Transform = target.transform;
switch (property) switch (property)
{ {
case UXBindingProperty.GameObjectActive: case UXBindingProperty.GameObjectActive:
@ -73,22 +95,32 @@ namespace UnityEngine.UI
case UXBindingProperty.CanvasGroupAlpha: case UXBindingProperty.CanvasGroupAlpha:
case UXBindingProperty.CanvasGroupInteractable: case UXBindingProperty.CanvasGroupInteractable:
case UXBindingProperty.CanvasGroupBlocksRaycasts: case UXBindingProperty.CanvasGroupBlocksRaycasts:
return target.GetComponent<CanvasGroup>() != null; return target.TryGetComponent(out resolvedTarget.CanvasGroup);
case UXBindingProperty.GraphicColor: case UXBindingProperty.GraphicColor:
case UXBindingProperty.GraphicMaterial: case UXBindingProperty.GraphicMaterial:
return target.GetComponent<Graphic>() != null; return target.TryGetComponent(out resolvedTarget.Graphic);
case UXBindingProperty.ImageSprite: case UXBindingProperty.ImageSprite:
return target.GetComponent<Image>() != null; return target.TryGetComponent(out resolvedTarget.Image);
case UXBindingProperty.TextContent: case UXBindingProperty.TextContent:
case UXBindingProperty.TextColor: case UXBindingProperty.TextColor:
return target.GetComponent<Text>() != null || target.GetComponent<TextMeshProUGUI>() != null; if (target.TryGetComponent(out resolvedTarget.Text))
{
return true;
}
return target.TryGetComponent(out resolvedTarget.TmpText);
case UXBindingProperty.RectTransformAnchoredPosition: case UXBindingProperty.RectTransformAnchoredPosition:
return target.GetComponent<RectTransform>() != null; return target.TryGetComponent(out resolvedTarget.RectTransform);
default: default:
return false; return false;
} }
} }
public static bool IsSupported(GameObject target, UXBindingProperty property)
{
return Resolve(target, property, out _);
}
public static void GetSupportedProperties(GameObject target, List<UXBindingProperty> output) public static void GetSupportedProperties(GameObject target, List<UXBindingProperty> output)
{ {
output.Clear(); output.Clear();
@ -107,127 +139,185 @@ namespace UnityEngine.UI
} }
} }
public static void CaptureValue(GameObject target, UXBindingProperty property, UXBindingValue destination) public static bool CaptureValue(GameObject target, UXBindingProperty property, UXBindingValue destination)
{ {
if (target == null || destination == null) if (!Resolve(target, property, out UXBindingResolvedTarget resolvedTarget))
{ {
return; return false;
}
return CaptureValue(in resolvedTarget, property, destination);
}
public static bool CaptureValue(in UXBindingResolvedTarget target, UXBindingProperty property, UXBindingValue destination)
{
if (destination == null || target.GameObject == null)
{
return false;
} }
switch (property) switch (property)
{ {
case UXBindingProperty.GameObjectActive: case UXBindingProperty.GameObjectActive:
destination.BoolValue = target.activeSelf; destination.BoolValue = target.GameObject.activeSelf;
return; return true;
case UXBindingProperty.CanvasGroupAlpha: case UXBindingProperty.CanvasGroupAlpha:
destination.FloatValue = target.GetComponent<CanvasGroup>().alpha; if (target.CanvasGroup == null) return false;
return; destination.FloatValue = target.CanvasGroup.alpha;
return true;
case UXBindingProperty.CanvasGroupInteractable: case UXBindingProperty.CanvasGroupInteractable:
destination.BoolValue = target.GetComponent<CanvasGroup>().interactable; if (target.CanvasGroup == null) return false;
return; destination.BoolValue = target.CanvasGroup.interactable;
return true;
case UXBindingProperty.CanvasGroupBlocksRaycasts: case UXBindingProperty.CanvasGroupBlocksRaycasts:
destination.BoolValue = target.GetComponent<CanvasGroup>().blocksRaycasts; if (target.CanvasGroup == null) return false;
return; destination.BoolValue = target.CanvasGroup.blocksRaycasts;
return true;
case UXBindingProperty.GraphicColor: case UXBindingProperty.GraphicColor:
destination.ColorValue = target.GetComponent<Graphic>().color; if (target.Graphic == null) return false;
return; destination.ColorValue = target.Graphic.color;
return true;
case UXBindingProperty.GraphicMaterial: case UXBindingProperty.GraphicMaterial:
destination.ObjectValue = target.GetComponent<Graphic>().material; if (target.Graphic == null) return false;
return; destination.ObjectValue = target.Graphic.defaultMaterial;
return true;
case UXBindingProperty.ImageSprite: case UXBindingProperty.ImageSprite:
destination.ObjectValue = target.GetComponent<Image>().sprite; if (target.Image == null) return false;
return; destination.ObjectValue = target.Image.sprite;
return true;
case UXBindingProperty.TextContent: case UXBindingProperty.TextContent:
if (target.TryGetComponent<Text>(out Text text)) if (target.Text != null)
{ {
destination.StringValue = text.text; destination.StringValue = target.Text.text;
return true;
} }
else if (target.TryGetComponent<TextMeshProUGUI>(out TextMeshProUGUI tmp))
if (target.TmpText != null)
{ {
destination.StringValue = tmp.text; destination.StringValue = target.TmpText.text;
return true;
} }
return;
return false;
case UXBindingProperty.TextColor: case UXBindingProperty.TextColor:
if (target.TryGetComponent<Text>(out Text legacyText)) if (target.Text != null)
{ {
destination.ColorValue = legacyText.color; destination.ColorValue = target.Text.color;
return true;
} }
else if (target.TryGetComponent<TextMeshProUGUI>(out TextMeshProUGUI tmpText))
if (target.TmpText != null)
{ {
destination.ColorValue = tmpText.color; destination.ColorValue = target.TmpText.color;
return true;
} }
return;
return false;
case UXBindingProperty.RectTransformAnchoredPosition: case UXBindingProperty.RectTransformAnchoredPosition:
destination.Vector2Value = target.GetComponent<RectTransform>().anchoredPosition; if (target.RectTransform == null) return false;
return; destination.Vector2Value = target.RectTransform.anchoredPosition;
return true;
case UXBindingProperty.TransformLocalScale: case UXBindingProperty.TransformLocalScale:
destination.Vector3Value = target.transform.localScale; if (target.Transform == null) return false;
return; destination.Vector3Value = target.Transform.localScale;
return true;
case UXBindingProperty.TransformLocalEulerAngles: case UXBindingProperty.TransformLocalEulerAngles:
destination.Vector3Value = target.transform.localEulerAngles; if (target.Transform == null) return false;
return; destination.Vector3Value = target.Transform.localEulerAngles;
return true;
default:
return false;
} }
} }
public static void ApplyValue(GameObject target, UXBindingProperty property, UXBindingValue value) public static bool ApplyValue(GameObject target, UXBindingProperty property, UXBindingValue value)
{ {
if (target == null || value == null) if (!Resolve(target, property, out UXBindingResolvedTarget resolvedTarget))
{ {
return; return false;
}
return ApplyValue(in resolvedTarget, property, value);
}
public static bool ApplyValue(in UXBindingResolvedTarget target, UXBindingProperty property, UXBindingValue value)
{
if (value == null || target.GameObject == null)
{
return false;
} }
switch (property) switch (property)
{ {
case UXBindingProperty.GameObjectActive: case UXBindingProperty.GameObjectActive:
target.SetActive(value.BoolValue); target.GameObject.SetActive(value.BoolValue);
return; return true;
case UXBindingProperty.CanvasGroupAlpha: case UXBindingProperty.CanvasGroupAlpha:
target.GetComponent<CanvasGroup>().alpha = value.FloatValue; if (target.CanvasGroup == null) return false;
return; target.CanvasGroup.alpha = value.FloatValue;
return true;
case UXBindingProperty.CanvasGroupInteractable: case UXBindingProperty.CanvasGroupInteractable:
target.GetComponent<CanvasGroup>().interactable = value.BoolValue; if (target.CanvasGroup == null) return false;
return; target.CanvasGroup.interactable = value.BoolValue;
return true;
case UXBindingProperty.CanvasGroupBlocksRaycasts: case UXBindingProperty.CanvasGroupBlocksRaycasts:
target.GetComponent<CanvasGroup>().blocksRaycasts = value.BoolValue; if (target.CanvasGroup == null) return false;
return; target.CanvasGroup.blocksRaycasts = value.BoolValue;
return true;
case UXBindingProperty.GraphicColor: case UXBindingProperty.GraphicColor:
target.GetComponent<Graphic>().color = value.ColorValue; if (target.Graphic == null) return false;
return; target.Graphic.color = value.ColorValue;
return true;
case UXBindingProperty.GraphicMaterial: case UXBindingProperty.GraphicMaterial:
target.GetComponent<Graphic>().material = value.ObjectValue as Material; if (target.Graphic == null) return false;
return; target.Graphic.material = value.ObjectValue as Material;
return true;
case UXBindingProperty.ImageSprite: case UXBindingProperty.ImageSprite:
target.GetComponent<Image>().sprite = value.ObjectValue as Sprite; if (target.Image == null) return false;
return; target.Image.sprite = value.ObjectValue as Sprite;
return true;
case UXBindingProperty.TextContent: case UXBindingProperty.TextContent:
if (target.TryGetComponent<Text>(out Text text)) if (target.Text != null)
{ {
text.text = value.StringValue; target.Text.text = value.StringValue;
return true;
} }
else if (target.TryGetComponent<TextMeshProUGUI>(out TextMeshProUGUI tmp))
if (target.TmpText != null)
{ {
tmp.text = value.StringValue; target.TmpText.text = value.StringValue;
return true;
} }
return;
return false;
case UXBindingProperty.TextColor: case UXBindingProperty.TextColor:
if (target.TryGetComponent<Text>(out Text legacyText)) if (target.Text != null)
{ {
legacyText.color = value.ColorValue; target.Text.color = value.ColorValue;
return true;
} }
else if (target.TryGetComponent<TextMeshProUGUI>(out TextMeshProUGUI tmpText))
if (target.TmpText != null)
{ {
tmpText.color = value.ColorValue; target.TmpText.color = value.ColorValue;
return true;
} }
return;
return false;
case UXBindingProperty.RectTransformAnchoredPosition: case UXBindingProperty.RectTransformAnchoredPosition:
target.GetComponent<RectTransform>().anchoredPosition = value.Vector2Value; if (target.RectTransform == null) return false;
return; target.RectTransform.anchoredPosition = value.Vector2Value;
return true;
case UXBindingProperty.TransformLocalScale: case UXBindingProperty.TransformLocalScale:
target.transform.localScale = value.Vector3Value; if (target.Transform == null) return false;
return; target.Transform.localScale = value.Vector3Value;
return true;
case UXBindingProperty.TransformLocalEulerAngles: case UXBindingProperty.TransformLocalEulerAngles:
target.transform.localEulerAngles = value.Vector3Value; if (target.Transform == null) return false;
return; target.Transform.localEulerAngles = value.Vector3Value;
return true;
default:
return false;
} }
} }
} }

View File

@ -17,6 +17,7 @@ namespace UnityEngine.UI
[SerializeField] private string _id = string.Empty; [SerializeField] private string _id = string.Empty;
[SerializeField] private string _name = "Controller"; [SerializeField] private string _name = "Controller";
[SerializeField] private int _length = 2; [SerializeField] private int _length = 2;
[SerializeField] private int _defaultIndex;
[SerializeField] private string _description = string.Empty; [SerializeField] private string _description = string.Empty;
[NonSerialized] private int _selectedIndex = -1; [NonSerialized] private int _selectedIndex = -1;
[NonSerialized] private UXController _owner; [NonSerialized] private UXController _owner;
@ -41,6 +42,12 @@ namespace UnityEngine.UI
set => _description = value; set => _description = value;
} }
public int DefaultIndex
{
get => Mathf.Clamp(_defaultIndex, 0, Length - 1);
set => _defaultIndex = Mathf.Clamp(value, 0, Length - 1);
}
public int SelectedIndex public int SelectedIndex
{ {
get => _selectedIndex; get => _selectedIndex;
@ -80,6 +87,15 @@ namespace UnityEngine.UI
private readonly Dictionary<string, int> _controllerIdMap = new Dictionary<string, int>(); private readonly Dictionary<string, int> _controllerIdMap = new Dictionary<string, int>();
private readonly Dictionary<string, int> _controllerNameMap = new Dictionary<string, int>(); private readonly Dictionary<string, int> _controllerNameMap = new Dictionary<string, int>();
private RuntimeBindingEntry[][] _runtimeEntriesByController = Array.Empty<RuntimeBindingEntry[]>();
private int[] _runtimeEntryCounts = Array.Empty<int>();
private bool _runtimeReady;
private struct RuntimeBindingEntry
{
public UXBinding Binding;
public int EntryIndex;
}
public IReadOnlyList<ControllerDefinition> Controllers public IReadOnlyList<ControllerDefinition> Controllers
{ {
@ -111,6 +127,18 @@ namespace UnityEngine.UI
return false; return false;
} }
internal bool TryGetControllerSlot(string controllerId, out int slot)
{
slot = -1;
if (string.IsNullOrEmpty(controllerId))
{
return false;
}
EnsureInitialized();
return _controllerIdMap.TryGetValue(controllerId, out slot);
}
public bool TryGetControllerByName(string controllerName, out ControllerDefinition controller) public bool TryGetControllerByName(string controllerName, out ControllerDefinition controller)
{ {
controller = null; controller = null;
@ -190,7 +218,11 @@ namespace UnityEngine.UI
for (int i = 0; i < _controllers.Count; i++) for (int i = 0; i < _controllers.Count; i++)
{ {
SetControllerIndexInternal(_controllers[i], 0, true); ControllerDefinition controller = _controllers[i];
if (controller != null)
{
SetControllerIndexInternal(controller, controller.DefaultIndex, true);
}
} }
} }
@ -209,6 +241,12 @@ namespace UnityEngine.UI
if (!_bindings.Contains(binding)) if (!_bindings.Contains(binding))
{ {
_bindings.Add(binding); _bindings.Add(binding);
_runtimeReady = false;
if (Application.isPlaying)
{
RebuildRuntimeEntries();
ApplyCurrentStateToBinding(binding);
}
#if UNITY_EDITOR #if UNITY_EDITOR
if (!Application.isPlaying) if (!Application.isPlaying)
{ {
@ -226,6 +264,7 @@ namespace UnityEngine.UI
} }
_bindings.Remove(binding); _bindings.Remove(binding);
_runtimeReady = false;
#if UNITY_EDITOR #if UNITY_EDITOR
if (!Application.isPlaying) if (!Application.isPlaying)
{ {
@ -252,10 +291,11 @@ namespace UnityEngine.UI
{ {
if (_bindings[i] != null) if (_bindings[i] != null)
{ {
_bindings[i].Initialize(); _bindings[i].RebuildRuntime(this);
} }
} }
RebuildRuntimeEntries();
ResetAllControllers(); ResetAllControllers();
} }
@ -278,7 +318,9 @@ namespace UnityEngine.UI
_controllerIdMap.Clear(); _controllerIdMap.Clear();
_controllerNameMap.Clear(); _controllerNameMap.Clear();
#if UNITY_EDITOR
var usedNames = new HashSet<string>(StringComparer.Ordinal); var usedNames = new HashSet<string>(StringComparer.Ordinal);
#endif
for (int i = 0; i < _controllers.Count; i++) for (int i = 0; i < _controllers.Count; i++)
{ {
@ -291,7 +333,9 @@ namespace UnityEngine.UI
controller.EnsureId(); controller.EnsureId();
controller.SetOwner(this); controller.SetOwner(this);
controller.SetSelectedIndexSilently(Mathf.Clamp(controller.SelectedIndex, -1, controller.Length - 1)); controller.SetSelectedIndexSilently(Mathf.Clamp(controller.SelectedIndex, -1, controller.Length - 1));
controller.DefaultIndex = controller.DefaultIndex;
#if UNITY_EDITOR
if (string.IsNullOrWhiteSpace(controller.Name)) if (string.IsNullOrWhiteSpace(controller.Name))
{ {
controller.Name = $"Controller{i + 1}"; controller.Name = $"Controller{i + 1}";
@ -302,10 +346,13 @@ namespace UnityEngine.UI
controller.Name = $"{controller.Name}_{i + 1}"; controller.Name = $"{controller.Name}_{i + 1}";
usedNames.Add(controller.Name); usedNames.Add(controller.Name);
} }
#endif
_controllerIdMap[controller.Id] = i; _controllerIdMap[controller.Id] = i;
_controllerNameMap[controller.Name] = i; _controllerNameMap[controller.Name] = i;
} }
_runtimeReady = false;
} }
private void CleanupBindings() private void CleanupBindings()
@ -333,18 +380,112 @@ namespace UnityEngine.UI
} }
controller.SetSelectedIndexSilently(selectedIndex); controller.SetSelectedIndexSilently(selectedIndex);
NotifyBindings(controller.Id, selectedIndex); if (TryGetControllerSlot(controller.Id, out int slot))
{
NotifyBindings(slot, selectedIndex);
}
return true; return true;
} }
private void NotifyBindings(string controllerId, int selectedIndex) private void NotifyBindings(int controllerSlot, int selectedIndex)
{ {
for (int i = 0; i < _bindings.Count; i++) if (!_runtimeReady)
{ {
UXBinding binding = _bindings[i]; RebuildRuntimeEntries();
}
if ((uint)controllerSlot >= (uint)_runtimeEntriesByController.Length)
{
return;
}
RuntimeBindingEntry[] entries = _runtimeEntriesByController[controllerSlot];
int count = _runtimeEntryCounts[controllerSlot];
for (int i = 0; i < count; i++)
{
UXBinding binding = entries[i].Binding;
if (binding != null) if (binding != null)
{ {
binding.OnControllerChanged(controllerId, selectedIndex); binding.ApplyRuntimeEntry(entries[i].EntryIndex, selectedIndex);
}
}
}
private void RebuildRuntimeEntries()
{
int controllerCount = _controllers.Count;
if (_runtimeEntriesByController.Length != controllerCount)
{
_runtimeEntriesByController = new RuntimeBindingEntry[controllerCount][];
_runtimeEntryCounts = new int[controllerCount];
}
for (int i = 0; i < controllerCount; i++)
{
_runtimeEntryCounts[i] = 0;
}
for (int bindingIndex = 0; bindingIndex < _bindings.Count; bindingIndex++)
{
UXBinding binding = _bindings[bindingIndex];
if (binding == null)
{
continue;
}
binding.RebuildRuntime(this);
for (int entryIndex = 0; entryIndex < binding.RuntimeEntryCount; entryIndex++)
{
int controllerSlot = binding.GetRuntimeControllerSlot(entryIndex);
if ((uint)controllerSlot >= (uint)controllerCount || !binding.IsRuntimeEntrySupported(entryIndex))
{
continue;
}
int nextIndex = _runtimeEntryCounts[controllerSlot];
RuntimeBindingEntry[] entries = _runtimeEntriesByController[controllerSlot];
if (entries == null || nextIndex >= entries.Length)
{
int nextLength = entries == null ? 4 : entries.Length << 1;
RuntimeBindingEntry[] nextEntries = new RuntimeBindingEntry[nextLength];
if (entries != null)
{
Array.Copy(entries, nextEntries, entries.Length);
}
entries = nextEntries;
_runtimeEntriesByController[controllerSlot] = entries;
}
entries[nextIndex].Binding = binding;
entries[nextIndex].EntryIndex = entryIndex;
_runtimeEntryCounts[controllerSlot] = nextIndex + 1;
}
}
_runtimeReady = true;
}
private void ApplyCurrentStateToBinding(UXBinding binding)
{
if (binding == null)
{
return;
}
for (int entryIndex = 0; entryIndex < binding.RuntimeEntryCount; entryIndex++)
{
int controllerSlot = binding.GetRuntimeControllerSlot(entryIndex);
if ((uint)controllerSlot >= (uint)_controllers.Count)
{
continue;
}
ControllerDefinition controller = _controllers[controllerSlot];
if (controller != null)
{
binding.ApplyRuntimeEntry(entryIndex, controller.SelectedIndex);
} }
} }
} }

View File

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.EventSystems;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems;
namespace UnityEngine.UI namespace UnityEngine.UI
{ {
@ -12,22 +9,34 @@ namespace UnityEngine.UI
{ {
[SerializeField] private bool m_AllowSwitchOff = false; [SerializeField] private bool m_AllowSwitchOff = false;
public bool allowSwitchOff
{
get { return m_AllowSwitchOff; }
set { m_AllowSwitchOff = value; }
}
[SerializeField] [SerializeField]
private List<UXToggle> m_Toggles = new List<UXToggle>(); private UXToggle[] m_Toggles = new UXToggle[0];
[SerializeField] [SerializeField]
private UXToggle m_DefaultToggle; private UXToggle m_DefaultToggle;
private int m_ToggleCount;
private UXToggle m_CurrentToggle;
private int m_CurrentIndex = -1;
public bool allowSwitchOff
{
get { return m_AllowSwitchOff; }
set
{
m_AllowSwitchOff = value;
EnsureValidState();
}
}
public UXToggle defaultToggle public UXToggle defaultToggle
{ {
get { return m_DefaultToggle; } get { return m_DefaultToggle; }
set { m_DefaultToggle = value; EnsureValidState(); } set
{
m_DefaultToggle = ContainsToggle(value) ? value : null;
EnsureValidState();
}
} }
protected UXGroup() protected UXGroup()
@ -46,157 +55,90 @@ namespace UnityEngine.UI
base.OnEnable(); 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) public void NotifyToggleOn(UXToggle toggle, bool sendCallback = true)
{ {
ValidateToggleIsInGroup(toggle); EnsureStorage();
for (var i = 0; i < m_Toggles.Count; i++) int index = IndexOfToggle(toggle);
if (index < 0)
return;
m_CurrentToggle = toggle;
m_CurrentIndex = index;
for (int i = 0; i < m_ToggleCount; i++)
{ {
if (m_Toggles[i] == toggle) UXToggle item = m_Toggles[i];
if (item == null || item == toggle)
continue; continue;
if (sendCallback) if (sendCallback)
m_Toggles[i].isOn = false; item.isOn = false;
else else
m_Toggles[i].SetIsOnWithoutNotify(false); item.SetIsOnWithoutNotify(false);
} }
} }
public void UnregisterToggle(UXToggle toggle) public void UnregisterToggle(UXToggle toggle)
{ {
if (toggle == null) EnsureStorage();
int index = IndexOfToggle(toggle);
if (index < 0)
return; return;
if (m_Toggles.Contains(toggle)) RemoveAt(index);
m_Toggles.Remove(toggle);
if (m_DefaultToggle == toggle) if (m_DefaultToggle == toggle)
{
m_DefaultToggle = null; m_DefaultToggle = null;
if (m_CurrentToggle == toggle)
{
m_CurrentToggle = null;
m_CurrentIndex = -1;
} }
EnsureSingleSelection();
} }
public void RegisterToggle(UXToggle toggle) public void RegisterToggle(UXToggle toggle)
{ {
if (toggle == null) EnsureStorage();
if (toggle == null || ContainsToggle(toggle))
return; return;
if (!m_Toggles.Contains(toggle)) EnsureCapacity(m_ToggleCount + 1);
m_Toggles.Add(toggle); m_Toggles[m_ToggleCount] = toggle;
m_ToggleCount++;
if (toggle.isOn)
if (!allowSwitchOff) NotifyToggleOn(toggle);
{ else
EnsureSingleSelection();
var firstActive = GetFirstActiveToggle();
if (firstActive != null && firstActive != toggle && toggle.isOn)
{
toggle.SetIsOnWithoutNotify(false);
}
else if (firstActive == null)
{
if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle))
{
var dt = m_DefaultToggle;
if (dt != null && dt != toggle)
{
dt.isOn = true;
NotifyToggleOn(dt);
}
else if (dt == toggle)
{
toggle.isOn = true;
NotifyToggleOn(toggle);
}
}
}
}
} }
public bool ContainsToggle(UXToggle toggle) public bool ContainsToggle(UXToggle toggle)
{ {
return m_Toggles != null && m_Toggles.Contains(toggle); EnsureStorage();
return IndexOfToggle(toggle) >= 0;
} }
public void EnsureValidState() public void EnsureValidState()
{ {
if (m_Toggles == null) EnsureStorage();
m_Toggles = new List<UXToggle>(); CompactNulls();
SyncToggleGroups();
m_Toggles.RemoveAll(x => x == null); EnsureDefaultToggle();
EnsureSingleSelection();
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);
}
}
IEnumerable<UXToggle> activeToggles = ActiveToggles();
if (activeToggles.Count() > 1)
{
UXToggle firstActive = GetFirstActiveToggle();
foreach (UXToggle toggle in activeToggles)
{
if (toggle == firstActive)
{
continue;
}
toggle.isOn = false;
}
}
for (int i = 0; i < m_Toggles.Count; i++)
{
var t = m_Toggles[i];
if (t == null)
continue;
if (t.group != this)
{
t.group = this;
}
}
} }
public bool AnyTogglesOn() public bool AnyTogglesOn()
{ {
return m_Toggles.Find(x => x != null && x.isOn) != null; return FindFirstActiveIndex() >= 0;
}
public IEnumerable<UXToggle> ActiveToggles()
{
return m_Toggles.Where(x => x != null && x.isOn);
} }
public UXToggle GetFirstActiveToggle() public UXToggle GetFirstActiveToggle()
{ {
if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle) && m_DefaultToggle.isOn) int index = FindFirstActiveIndex();
return m_DefaultToggle; return index >= 0 ? m_Toggles[index] : null;
IEnumerable<UXToggle> activeToggles = ActiveToggles();
return activeToggles.Count() > 0 ? activeToggles.First() : null;
} }
public void SetAllTogglesOff(bool sendCallback = true) public void SetAllTogglesOff(bool sendCallback = true)
@ -204,19 +146,20 @@ namespace UnityEngine.UI
bool oldAllowSwitchOff = m_AllowSwitchOff; bool oldAllowSwitchOff = m_AllowSwitchOff;
m_AllowSwitchOff = true; m_AllowSwitchOff = true;
if (sendCallback) for (int i = 0; i < m_ToggleCount; i++)
{ {
for (var i = 0; i < m_Toggles.Count; i++) UXToggle toggle = m_Toggles[i];
if (m_Toggles[i] != null) if (toggle == null)
m_Toggles[i].isOn = false; continue;
}
else if (sendCallback)
{ toggle.isOn = false;
for (var i = 0; i < m_Toggles.Count; i++) else
if (m_Toggles[i] != null) toggle.SetIsOnWithoutNotify(false);
m_Toggles[i].SetIsOnWithoutNotify(false);
} }
m_CurrentToggle = null;
m_CurrentIndex = -1;
m_AllowSwitchOff = oldAllowSwitchOff; m_AllowSwitchOff = oldAllowSwitchOff;
} }
@ -225,47 +168,229 @@ namespace UnityEngine.UI
SelectAdjacent(true); SelectAdjacent(true);
} }
public void Preview() public void Previous()
{ {
SelectAdjacent(false); SelectAdjacent(false);
} }
private void SelectAdjacent(bool forward) internal UXToggle GetToggleAt(int index)
{ {
if (m_Toggles == null || m_Toggles.Count == 0) return index >= 0 && index < m_ToggleCount ? m_Toggles[index] : null;
}
internal int ToggleCount
{
get { return m_ToggleCount; }
}
private void EnsureStorage()
{
if (m_Toggles == null)
m_Toggles = new UXToggle[0];
if (m_ToggleCount == 0)
{
int count = 0;
for (int i = 0; i < m_Toggles.Length; i++)
{
if (m_Toggles[i] != null)
count = i + 1;
}
m_ToggleCount = count;
}
if (m_ToggleCount > m_Toggles.Length)
m_ToggleCount = m_Toggles.Length;
}
private void CompactNulls()
{
int write = 0;
for (int read = 0; read < m_Toggles.Length; read++)
{
UXToggle toggle = m_Toggles[read];
if (toggle == null)
continue;
m_Toggles[write] = toggle;
write++;
}
for (int i = write; i < m_Toggles.Length; i++)
m_Toggles[i] = null;
m_ToggleCount = write;
}
private void SyncToggleGroups()
{
for (int i = 0; i < m_ToggleCount; i++)
{
UXToggle toggle = m_Toggles[i];
if (toggle != null && toggle.group != this)
toggle.SetToggleGroupInternal(this, true);
}
}
private void EnsureDefaultToggle()
{
if (m_ToggleCount == 0)
{
m_DefaultToggle = null;
return;
}
if (m_DefaultToggle != null && IndexOfToggle(m_DefaultToggle) >= 0)
return; return;
UXToggle current = GetFirstActiveToggle(); if (!m_AllowSwitchOff)
int currentIndex = current != null ? m_Toggles.IndexOf(current) : -1; m_DefaultToggle = m_Toggles[0];
else
m_DefaultToggle = null;
}
int idx = currentIndex; private void EnsureSingleSelection()
if (idx == -1 && !forward) {
idx = 0; int selectedIndex = FindSelectedIndex();
if (selectedIndex < 0 && !m_AllowSwitchOff && m_ToggleCount > 0)
for (int step = 0; step < m_Toggles.Count; step++)
{ {
if (forward) selectedIndex = GetDefaultIndex();
if (selectedIndex < 0)
selectedIndex = 0;
UXToggle toggle = m_Toggles[selectedIndex];
if (toggle != null)
toggle.isOn = true;
}
if (selectedIndex >= 0)
{
m_CurrentToggle = m_Toggles[selectedIndex];
m_CurrentIndex = selectedIndex;
for (int i = 0; i < m_ToggleCount; i++)
{ {
idx = (idx + 1) % m_Toggles.Count; UXToggle toggle = m_Toggles[i];
} if (toggle != null && i != selectedIndex && toggle.isOn)
else toggle.SetIsOnWithoutNotify(false);
{
idx = (idx - 1 + m_Toggles.Count) % m_Toggles.Count;
} }
}
else
{
m_CurrentToggle = null;
m_CurrentIndex = -1;
}
}
UXToggle t = m_Toggles[idx]; private int FindSelectedIndex()
if (t == null) {
int defaultIndex = GetDefaultIndex();
if (defaultIndex >= 0 && m_Toggles[defaultIndex].isOn)
return defaultIndex;
return FindFirstActiveIndex();
}
private int FindFirstActiveIndex()
{
for (int i = 0; i < m_ToggleCount; i++)
{
UXToggle toggle = m_Toggles[i];
if (toggle != null && toggle.isOn)
return i;
}
return -1;
}
private int GetDefaultIndex()
{
if (m_DefaultToggle == null)
return -1;
return IndexOfToggle(m_DefaultToggle);
}
private int IndexOfToggle(UXToggle toggle)
{
if (toggle == null || m_Toggles == null)
return -1;
for (int i = 0; i < m_ToggleCount; i++)
{
if (m_Toggles[i] == toggle)
return i;
}
return -1;
}
private void EnsureCapacity(int capacity)
{
if (m_Toggles == null)
m_Toggles = new UXToggle[capacity < 4 ? 4 : capacity];
if (m_Toggles.Length >= capacity)
return;
int newCapacity = m_Toggles.Length == 0 ? 4 : m_Toggles.Length * 2;
while (newCapacity < capacity)
newCapacity *= 2;
UXToggle[] newToggles = new UXToggle[newCapacity];
for (int i = 0; i < m_ToggleCount; i++)
newToggles[i] = m_Toggles[i];
m_Toggles = newToggles;
}
private void RemoveAt(int index)
{
int lastIndex = m_ToggleCount - 1;
for (int i = index; i < lastIndex; i++)
m_Toggles[i] = m_Toggles[i + 1];
if (lastIndex >= 0)
m_Toggles[lastIndex] = null;
m_ToggleCount = lastIndex;
}
private void SelectAdjacent(bool forward)
{
if (m_ToggleCount == 0)
return;
int index = ResolveCurrentIndex();
if (index < 0)
index = forward ? -1 : 0;
for (int step = 0; step < m_ToggleCount; step++)
{
index = forward ? index + 1 : index - 1;
if (index >= m_ToggleCount)
index = 0;
else if (index < 0)
index = m_ToggleCount - 1;
UXToggle toggle = m_Toggles[index];
if (toggle == null || !toggle.IsActive() || !toggle.IsInteractable())
continue; continue;
if (!t.IsActive()) toggle.isOn = true;
continue;
if (!t.IsInteractable())
continue;
t.isOn = true;
return; return;
} }
} }
private int ResolveCurrentIndex()
{
if (m_CurrentIndex >= 0 && m_CurrentIndex < m_ToggleCount && m_Toggles[m_CurrentIndex] == m_CurrentToggle)
return m_CurrentIndex;
m_CurrentIndex = FindFirstActiveIndex();
m_CurrentToggle = m_CurrentIndex >= 0 ? m_Toggles[m_CurrentIndex] : null;
return m_CurrentIndex;
}
} }
} }

View File

@ -5,7 +5,7 @@ MonoImporter:
serializedVersion: 2 serializedVersion: 2
defaultReferences: [] defaultReferences: []
executionOrder: 0 executionOrder: 0
icon: {fileID: 2800000, guid: 42b2d97a2cb439b4395c6dca63357d89, type: 3} icon: {instanceID: 0}
userData: userData:
assetBundleName: assetBundleName:
assetBundleVariant: assetBundleVariant:

View File

@ -51,7 +51,7 @@ namespace UnityEngine.UI
{ {
#if UNITY_EDITOR #if UNITY_EDITOR
if (executing == CanvasUpdate.Prelayout) if (executing == CanvasUpdate.Prelayout)
onValueChanged.Invoke(m_IsOn); PlayEffect(true);
#endif #endif
} }
@ -66,13 +66,24 @@ namespace UnityEngine.UI
protected override void OnDestroy() protected override void OnDestroy()
{ {
if (m_Group != null) if (m_Group != null)
m_Group.EnsureValidState(); m_Group.UnregisterToggle(this);
base.OnDestroy(); base.OnDestroy();
} }
protected override void OnDisable()
{
if (m_Group != null)
m_Group.UnregisterToggle(this);
base.OnDisable();
}
protected override void OnEnable() protected override void OnEnable()
{ {
base.OnEnable(); base.OnEnable();
if (m_Group != null)
SetToggleGroup(m_Group, false);
PlayEffect(true); PlayEffect(true);
} }
@ -92,7 +103,13 @@ namespace UnityEngine.UI
} }
private void SetToggleGroup(UXGroup newGroup, bool setMemberValue) internal void SetToggleGroupInternal(UXGroup newGroup, bool setMemberValue)
{
if (setMemberValue)
m_Group = newGroup;
}
protected virtual void SetToggleGroup(UXGroup newGroup, bool setMemberValue)
{ {
if (m_Group == newGroup) if (m_Group == newGroup)
{ {
@ -155,11 +172,13 @@ namespace UnityEngine.UI
base.DoStateTransition(state, instant); base.DoStateTransition(state, instant);
} }
void Set(bool value, bool sendCallback = true) protected virtual void Set(bool value, bool sendCallback = true)
{ {
if (m_IsOn == value) if (m_IsOn == value)
return; return;
OnBeforeValueChanged(value);
m_IsOn = value; m_IsOn = value;
if (m_Group != null && m_Group.isActiveAndEnabled && IsActive()) if (m_Group != null && m_Group.isActiveAndEnabled && IsActive())
{ {
@ -182,20 +201,33 @@ namespace UnityEngine.UI
var stateToApply = m_IsOn ? Selectable.SelectionState.Selected : currentSelectionState; var stateToApply = m_IsOn ? Selectable.SelectionState.Selected : currentSelectionState;
DoStateTransition(stateToApply, instant); DoStateTransition(stateToApply, instant);
OnAfterValueChanged(m_IsOn);
} }
protected virtual void OnBeforeValueChanged(bool value)
{
}
private void PlayEffect(bool instant) protected virtual void OnAfterValueChanged(bool value)
{
}
protected virtual void PlayEffect(bool instant)
{ {
if (graphic == null) if (graphic == null)
return; return;
float alpha = m_IsOn ? 1f : 0f;
#if UNITY_EDITOR #if UNITY_EDITOR
if (!Application.isPlaying) if (!Application.isPlaying)
graphic.canvasRenderer.SetAlpha(m_IsOn ? 1f : 0f); graphic.canvasRenderer.SetAlpha(alpha);
else else
#endif #endif
graphic.CrossFadeAlpha(m_IsOn ? 1f : 0f, instant ? 0f : 0.1f, true); if (instant)
graphic.canvasRenderer.SetAlpha(alpha);
else
graphic.CrossFadeAlpha(alpha, 0.1f, true);
} }
@ -230,6 +262,9 @@ namespace UnityEngine.UI
public override void OnSelect(BaseEventData eventData) public override void OnSelect(BaseEventData eventData)
{ {
base.OnSelect(eventData); base.OnSelect(eventData);
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
UXNavigationRuntime.NotifySelection(gameObject);
#endif
if (eventData is PointerEventData) if (eventData is PointerEventData)
return; return;
PlayAudio(hoverAudioClip); PlayAudio(hoverAudioClip);

View File

@ -1,4 +1,5 @@
#if INPUTSYSTEM_SUPPORT #if INPUTSYSTEM_SUPPORT
using AlicizaX.UI.Runtime;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
@ -8,29 +9,42 @@ namespace UnityEngine.UI
public sealed class HotkeyComponent : MonoBehaviour, IHotkeyTrigger public sealed class HotkeyComponent : MonoBehaviour, IHotkeyTrigger
{ {
[SerializeField] private Component _component; [SerializeField] private Component _component;
[SerializeField] private UIHolderObjectBase _holder;
[SerializeField] private InputActionReference _hotkeyAction; [SerializeField] private InputActionReference _hotkeyAction;
[SerializeField] private EHotkeyPressType _hotkeyPressType = EHotkeyPressType.Performed; [SerializeField] private EHotkeyPressType _hotkeyPressType = EHotkeyPressType.Performed;
private ISubmitHandler _submitHandler;
private BaseEventData _eventData;
private EventSystem _eventSystem;
public InputActionReference HotkeyAction public InputActionReference HotkeyAction
{ {
get => _hotkeyAction; get => _hotkeyAction;
set => _hotkeyAction = value; set => _hotkeyAction = value;
} }
EHotkeyPressType IHotkeyTrigger.HotkeyPressType public EHotkeyPressType HotkeyPressType => _hotkeyPressType;
{ public UIHolderObjectBase HotkeyHolder => _holder;
get => _hotkeyPressType;
set => _hotkeyPressType = value;
}
private void Reset() private void Reset()
{ {
AutoAssignTarget(); AutoAssignTarget();
AutoAssignHolder();
}
private void Awake()
{
AutoAssignTarget();
AutoAssignHolder();
CacheTarget();
CacheEventData();
} }
private void OnEnable() private void OnEnable()
{ {
AutoAssignTarget(); AutoAssignTarget();
AutoAssignHolder();
CacheTarget();
((IHotkeyTrigger)this).BindHotKey(); ((IHotkeyTrigger)this).BindHotKey();
} }
@ -39,36 +53,54 @@ namespace UnityEngine.UI
((IHotkeyTrigger)this).UnBindHotKey(); ((IHotkeyTrigger)this).UnBindHotKey();
} }
private void OnApplicationFocus(bool hasFocus)
{
if (hasFocus)
{
CacheEventData();
}
}
private void OnDestroy() private void OnDestroy()
{ {
((IHotkeyTrigger)this).UnBindHotKey(); ((IHotkeyTrigger)this).UnBindHotKey();
_submitHandler = null;
_eventData = null;
_eventSystem = null;
} }
#if UNITY_EDITOR #if UNITY_EDITOR
private void OnValidate() private void OnValidate()
{ {
AutoAssignTarget(); AutoAssignTarget();
AutoAssignHolder();
CacheTarget();
if (_component != null && _submitHandler == null)
{
_component = null;
}
} }
#endif #endif
void IHotkeyTrigger.HotkeyActionTrigger() public void HotkeyActionTrigger()
{ {
if (!isActiveAndEnabled || _component == null) if (!isActiveAndEnabled || _submitHandler == null)
{ {
return; return;
} }
if (_component is ISubmitHandler) if (_eventData == null)
{ {
ExecuteEvents.Execute(
_component.gameObject,
new BaseEventData(EventSystem.current),
ExecuteEvents.submitHandler
);
return; return;
} }
Debug.LogWarning($"{nameof(HotkeyComponent)} target must implement {nameof(ISubmitHandler)}: {_component.name}", this); if (!ReferenceEquals(_eventSystem, EventSystem.current))
{
return;
}
_submitHandler.OnSubmit(_eventData);
} }
private void AutoAssignTarget() private void AutoAssignTarget()
@ -83,6 +115,27 @@ namespace UnityEngine.UI
_component = submitHandler; _component = submitHandler;
} }
} }
private void AutoAssignHolder()
{
if (_holder != null && _holder.IsValid())
{
return;
}
_holder = GetComponentInParent<UIHolderObjectBase>(true);
}
private void CacheTarget()
{
_submitHandler = _component as ISubmitHandler;
}
private void CacheEventData()
{
_eventSystem = EventSystem.current;
_eventData = new BaseEventData(_eventSystem);
}
} }
} }
#endif #endif

View File

@ -1,13 +1,15 @@
#if INPUTSYSTEM_SUPPORT #if INPUTSYSTEM_SUPPORT
using AlicizaX.UI.Runtime;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
namespace UnityEngine.UI namespace UnityEngine.UI
{ {
public interface IHotkeyTrigger public interface IHotkeyTrigger
{ {
public InputActionReference HotkeyAction { get; } InputActionReference HotkeyAction { get; }
internal EHotkeyPressType HotkeyPressType { get; set; } EHotkeyPressType HotkeyPressType { get; }
internal void HotkeyActionTrigger(); UIHolderObjectBase HotkeyHolder { get; }
void HotkeyActionTrigger();
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 0aa77908962c48199d63710fa15b8c37 guid: 0aa77908962c48199d63710fa15b8c37
timeCreated: 1754555268 MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,9 @@ namespace UnityEngine.UI
public enum UXInputMode : byte public enum UXInputMode : byte
{ {
Pointer = 0, Pointer = 0,
Gamepad = 1 Keyboard = 1,
Gamepad = 2,
Touch = 3
} }
} }
#endif #endif

View File

@ -7,10 +7,15 @@ namespace UnityEngine.UI
{ {
internal sealed class UXInputModeService : MonoBehaviour internal sealed class UXInputModeService : MonoBehaviour
{ {
private const float StickThresholdSqr = 0.04f;
private const float AxisThreshold = 0.2f;
private static UXInputModeService _instance; private static UXInputModeService _instance;
private InputAction _pointerAction; private InputAction _pointerAction;
private InputAction _keyboardAction;
private InputAction _gamepadAction; private InputAction _gamepadAction;
private InputAction _touchAction;
public static UXInputMode CurrentMode { get; private set; } = UXInputMode.Pointer; public static UXInputMode CurrentMode { get; private set; } = UXInputMode.Pointer;
@ -29,7 +34,7 @@ namespace UnityEngine.UI
return _instance; return _instance;
} }
var go = new GameObject("[UXInputModeService]"); GameObject go = new GameObject("[UXInputModeService]");
go.hideFlags = HideFlags.HideAndDontSave; go.hideFlags = HideFlags.HideAndDontSave;
DontDestroyOnLoad(go); DontDestroyOnLoad(go);
_instance = go.AddComponent<UXInputModeService>(); _instance = go.AddComponent<UXInputModeService>();
@ -47,16 +52,17 @@ namespace UnityEngine.UI
_instance = this; _instance = this;
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject);
hideFlags = HideFlags.HideAndDontSave; hideFlags = HideFlags.HideAndDontSave;
CreateActions();
} }
private void OnEnable() private void OnEnable()
{ {
CreateActions(); SetActionsEnabled(true);
} }
private void OnDisable() private void OnDisable()
{ {
DisposeActions(); SetActionsEnabled(false);
} }
private void OnDestroy() private void OnDestroy()
@ -70,26 +76,63 @@ namespace UnityEngine.UI
private void CreateActions() private void CreateActions()
{ {
if (_pointerAction != null || _gamepadAction != null) if (_pointerAction != null)
{ {
return; return;
} }
_pointerAction = new InputAction("UXPointerInput", InputActionType.PassThrough); _pointerAction = new InputAction("UXPointerInput", InputActionType.PassThrough);
_pointerAction.AddBinding("<Keyboard>/anyKey");
_pointerAction.AddBinding("<Mouse>/delta"); _pointerAction.AddBinding("<Mouse>/delta");
_pointerAction.AddBinding("<Mouse>/scroll"); _pointerAction.AddBinding("<Mouse>/scroll");
_pointerAction.AddBinding("<Mouse>/leftButton"); _pointerAction.AddBinding("<Mouse>/leftButton");
_pointerAction.AddBinding("<Mouse>/rightButton"); _pointerAction.AddBinding("<Mouse>/rightButton");
_pointerAction.AddBinding("<Mouse>/middleButton"); _pointerAction.AddBinding("<Mouse>/middleButton");
_pointerAction.performed += OnPointerInput; _pointerAction.performed += OnPointerInput;
_pointerAction.Enable();
_keyboardAction = new InputAction("UXKeyboardInput", InputActionType.PassThrough);
_keyboardAction.AddBinding("<Keyboard>/anyKey");
_keyboardAction.performed += OnKeyboardInput;
_gamepadAction = new InputAction("UXGamepadInput", InputActionType.PassThrough); _gamepadAction = new InputAction("UXGamepadInput", InputActionType.PassThrough);
_gamepadAction.AddBinding("<Gamepad>/*"); _gamepadAction.AddBinding("<Gamepad>/buttonSouth");
_gamepadAction.AddBinding("<Joystick>/*"); _gamepadAction.AddBinding("<Gamepad>/buttonNorth");
_gamepadAction.AddBinding("<Gamepad>/buttonEast");
_gamepadAction.AddBinding("<Gamepad>/buttonWest");
_gamepadAction.AddBinding("<Gamepad>/startButton");
_gamepadAction.AddBinding("<Gamepad>/selectButton");
_gamepadAction.AddBinding("<Gamepad>/leftShoulder");
_gamepadAction.AddBinding("<Gamepad>/rightShoulder");
_gamepadAction.AddBinding("<Gamepad>/dpad");
_gamepadAction.AddBinding("<Gamepad>/leftStick");
_gamepadAction.AddBinding("<Gamepad>/rightStick");
_gamepadAction.performed += OnGamepadInput; _gamepadAction.performed += OnGamepadInput;
_gamepadAction.Enable();
_touchAction = new InputAction("UXTouchInput", InputActionType.PassThrough);
_touchAction.AddBinding("<Touchscreen>/primaryTouch/press");
_touchAction.AddBinding("<Touchscreen>/primaryTouch/delta");
_touchAction.performed += OnTouchInput;
}
private void SetActionsEnabled(bool enabled)
{
if (_pointerAction == null)
{
return;
}
if (enabled)
{
_pointerAction.Enable();
_keyboardAction.Enable();
_gamepadAction.Enable();
_touchAction.Enable();
return;
}
_pointerAction.Disable();
_keyboardAction.Disable();
_gamepadAction.Disable();
_touchAction.Disable();
} }
private void DisposeActions() private void DisposeActions()
@ -97,38 +140,62 @@ namespace UnityEngine.UI
if (_pointerAction != null) if (_pointerAction != null)
{ {
_pointerAction.performed -= OnPointerInput; _pointerAction.performed -= OnPointerInput;
_pointerAction.Disable();
_pointerAction.Dispose(); _pointerAction.Dispose();
_pointerAction = null; _pointerAction = null;
} }
if (_keyboardAction != null)
{
_keyboardAction.performed -= OnKeyboardInput;
_keyboardAction.Dispose();
_keyboardAction = null;
}
if (_gamepadAction != null) if (_gamepadAction != null)
{ {
_gamepadAction.performed -= OnGamepadInput; _gamepadAction.performed -= OnGamepadInput;
_gamepadAction.Disable();
_gamepadAction.Dispose(); _gamepadAction.Dispose();
_gamepadAction = null; _gamepadAction = null;
} }
if (_touchAction != null)
{
_touchAction.performed -= OnTouchInput;
_touchAction.Dispose();
_touchAction = null;
}
} }
private static void OnPointerInput(InputAction.CallbackContext context) private static void OnPointerInput(InputAction.CallbackContext context)
{ {
if (!IsInputMeaningful(context.control)) if (IsInputMeaningful(context.control))
{ {
return; SetMode(UXInputMode.Pointer);
} }
}
SetMode(UXInputMode.Pointer); private static void OnKeyboardInput(InputAction.CallbackContext context)
{
if (IsInputMeaningful(context.control))
{
SetMode(UXInputMode.Keyboard);
}
} }
private static void OnGamepadInput(InputAction.CallbackContext context) private static void OnGamepadInput(InputAction.CallbackContext context)
{ {
if (!IsInputMeaningful(context.control)) if (IsInputMeaningful(context.control))
{ {
return; SetMode(UXInputMode.Gamepad);
} }
}
SetMode(UXInputMode.Gamepad); private static void OnTouchInput(InputAction.CallbackContext context)
{
if (IsInputMeaningful(context.control))
{
SetMode(UXInputMode.Touch);
}
} }
private static bool IsInputMeaningful(InputControl control) private static bool IsInputMeaningful(InputControl control)
@ -143,11 +210,11 @@ namespace UnityEngine.UI
case ButtonControl button: case ButtonControl button:
return button.IsPressed(); return button.IsPressed();
case StickControl stick: case StickControl stick:
return stick.ReadValue().sqrMagnitude >= 0.04f; return stick.ReadValue().sqrMagnitude >= StickThresholdSqr;
case Vector2Control vector2: case Vector2Control vector2:
return vector2.ReadValue().sqrMagnitude >= 0.04f; return vector2.ReadValue().sqrMagnitude >= StickThresholdSqr;
case AxisControl axis: case AxisControl axis:
return Mathf.Abs(axis.ReadValue()) >= 0.2f; return Mathf.Abs(axis.ReadValue()) >= AxisThreshold;
default: default:
return !control.noisy; return !control.noisy;
} }

View File

@ -1,20 +0,0 @@
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
namespace UnityEngine.UI
{
[DisallowMultipleComponent]
internal sealed class UXNavigationLayerWatcher : MonoBehaviour
{
private UXNavigationRuntime _runtime;
internal void Initialize(UXNavigationRuntime runtime)
{
_runtime = runtime;
}
private void OnTransformChildrenChanged()
{
_runtime?.MarkDiscoveryDirty();
}
}
}
#endif

View File

@ -1,35 +1,42 @@
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION #if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
using System.Collections.Generic;
using AlicizaX; using AlicizaX;
using AlicizaX.UI.Runtime; using AlicizaX.UI.Runtime;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
namespace UnityEngine.UI namespace UnityEngine.UI
{ {
internal sealed class UXNavigationRuntime : MonoBehaviour public interface IUXNavigationCursorPolicy
{ {
void OnInputModeChanged(UXInputMode mode, UXNavigationScope topScope);
}
public sealed class UXNavigationRuntime : MonoBehaviour
{
private const int InitialScopeCapacity = 64;
private const int InvalidIndex = -1;
private static UXNavigationRuntime _instance; private static UXNavigationRuntime _instance;
private static readonly string CacheLayerName = $"Layer{(int)UILayer.All}-{UILayer.All}"; private static IUXNavigationCursorPolicy _cursorPolicy;
private readonly HashSet<UXNavigationScope> _scopeSet = new(); private UXNavigationScope[] _scopes = new UXNavigationScope[InitialScopeCapacity];
private readonly List<UXNavigationScope> _scopes = new(32); private int[] _freeIndices = new int[InitialScopeCapacity];
private readonly HashSet<Transform> _interactiveLayerRoots = new(); private int _freeCount;
private readonly List<UIHolderObjectBase> _holderBuffer = new(32); private int _scopeCount;
private int _scopeCapacityHighWater;
private Transform _uiCanvasRoot;
private UXNavigationScope _topScope; private UXNavigationScope _topScope;
private GameObject _lastObservedSelection;
private bool _discoveryDirty = true;
private ulong _activationSerial; private ulong _activationSerial;
private bool _missingEventSystemLogged; private bool _stateDirty = true;
private bool _topScopeDirty = true;
private bool _suppressionDirty = true; private bool _suppressionDirty = true;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Bootstrap() private static void Bootstrap()
{ {
if (!AppServices.TryGetApp<IUIService>(out var uiService) || uiService == null) return; if (!AppServices.TryGetApp<IUIService>(out var uiService) || uiService == null)
{
return;
}
EnsureInstance(); EnsureInstance();
UXInputModeService.EnsureInstance(); UXInputModeService.EnsureInstance();
} }
@ -41,7 +48,7 @@ namespace UnityEngine.UI
return _instance; return _instance;
} }
var go = new GameObject("[UXNavigationRuntime]"); GameObject go = new GameObject("[UXNavigationRuntime]");
go.hideFlags = HideFlags.HideAndDontSave; go.hideFlags = HideFlags.HideAndDontSave;
DontDestroyOnLoad(go); DontDestroyOnLoad(go);
_instance = go.AddComponent<UXNavigationRuntime>(); _instance = go.AddComponent<UXNavigationRuntime>();
@ -54,14 +61,33 @@ namespace UnityEngine.UI
return runtime != null; return runtime != null;
} }
internal static bool IsHolderWithinTopScope(UIHolderObjectBase holder) public static void SetCursorPolicy(IUXNavigationCursorPolicy cursorPolicy)
{
_cursorPolicy = cursorPolicy;
}
public static void NotifySelection(GameObject selectedObject)
{
if (_instance != null)
{
_instance.RecordSelection(selectedObject);
}
}
public static bool IsHolderWithinTopScope(UIHolderObjectBase holder)
{ {
if (_instance == null || _instance._topScope == null || holder == null) if (_instance == null || _instance._topScope == null || holder == null)
{ {
return true; return true;
} }
return _instance.IsHolderOwnedByScope(holder, _instance._topScope); UXNavigationScope scope = holder.GetComponent<UXNavigationScope>();
if (scope == null)
{
scope = holder.GetComponentInParent<UXNavigationScope>(true);
}
return scope == _instance._topScope;
} }
private void Awake() private void Awake()
@ -98,19 +124,42 @@ namespace UnityEngine.UI
internal void RegisterScope(UXNavigationScope scope) internal void RegisterScope(UXNavigationScope scope)
{ {
if (scope == null || !_scopeSet.Add(scope)) if (scope == null || scope.RuntimeIndex != InvalidIndex)
{ {
return; return;
} }
_scopes.Add(scope); int index;
_topScopeDirty = true; if (_freeCount > 0)
_suppressionDirty = true; {
index = _freeIndices[--_freeCount];
}
else
{
if (_scopeCapacityHighWater >= _scopes.Length)
{
ReportCapacityExceeded("UXNavigationRuntime scope capacity exceeded.");
return;
}
index = _scopeCapacityHighWater++;
}
_scopes[index] = scope;
scope.RuntimeIndex = index;
_scopeCount++;
MarkStateDirty();
} }
internal void UnregisterScope(UXNavigationScope scope) internal void UnregisterScope(UXNavigationScope scope)
{ {
if (scope == null || !_scopeSet.Remove(scope)) if (scope == null)
{
return;
}
int index = scope.RuntimeIndex;
if (index < 0 || index >= _scopes.Length || _scopes[index] != scope)
{ {
return; return;
} }
@ -121,42 +170,46 @@ namespace UnityEngine.UI
} }
scope.IsAvailable = false; scope.IsAvailable = false;
scope.WasAvailable = false;
scope.SetNavigationSuppressed(false); scope.SetNavigationSuppressed(false);
scope.RuntimeIndex = InvalidIndex;
_scopes[index] = null;
_freeIndices[_freeCount++] = index;
_scopeCount--;
MarkStateDirty();
}
int idx = _scopes.IndexOf(scope); internal void MarkStateDirty()
if (idx >= 0) {
{ _stateDirty = true;
int last = _scopes.Count - 1;
_scopes[idx] = _scopes[last];
_scopes.RemoveAt(last);
}
_topScopeDirty = true;
_suppressionDirty = true; _suppressionDirty = true;
} }
internal void MarkDiscoveryDirty() internal void MarkSuppressionDirty()
{ {
_discoveryDirty = true; _suppressionDirty = true;
} }
private void Update() internal void InvalidateSkipCaches()
{ {
TryBindUIRoot(); for (int i = 0; i < _scopeCapacityHighWater; i++)
if (_uiCanvasRoot == null)
{ {
return; UXNavigationScope scope = _scopes[i];
if (scope != null)
{
scope.InvalidateSkipCacheOnly();
}
} }
if (_discoveryDirty) MarkStateDirty();
{ }
DiscoverScopes();
}
if (_topScopeDirty) private void FlushStateIfDirty()
{
if (_stateDirty)
{ {
UXNavigationScope newTopScope = FindTopScope(); UXNavigationScope newTopScope = FindTopScope();
_topScopeDirty = false; _stateDirty = false;
if (!ReferenceEquals(_topScope, newTopScope)) if (!ReferenceEquals(_topScope, newTopScope))
{ {
_topScope = newTopScope; _topScope = newTopScope;
@ -170,122 +223,16 @@ namespace UnityEngine.UI
_suppressionDirty = false; _suppressionDirty = false;
} }
if (UXInputModeService.CurrentMode == UXInputMode.Gamepad) if (UXInputModeService.CurrentMode == UXInputMode.Gamepad || UXInputModeService.CurrentMode == UXInputMode.Keyboard)
{ {
EnsureGamepadSelection(); EnsureNavigationSelection();
if (_topScope != null)
{
Cursor.visible = false;
}
} }
TrackSelection();
}
private void TryBindUIRoot()
{
if (_uiCanvasRoot != null)
{
return;
}
IUIService uiModule = AppServices.RequireApp<IUIService>();
if (uiModule?.UICanvasRoot == null)
{
return;
}
_uiCanvasRoot = uiModule.UICanvasRoot;
EnsureWatcher(_uiCanvasRoot);
CacheInteractiveLayers();
DiscoverScopes();
}
private void CacheInteractiveLayers()
{
_interactiveLayerRoots.Clear();
if (_uiCanvasRoot == null)
{
return;
}
EnsureWatcher(_uiCanvasRoot);
for (int i = 0; i < _uiCanvasRoot.childCount; i++)
{
Transform child = _uiCanvasRoot.GetChild(i);
if (child == null || child.name == CacheLayerName)
{
continue;
}
_interactiveLayerRoots.Add(child);
EnsureWatcher(child);
}
}
private void DiscoverScopes()
{
_discoveryDirty = false;
CacheInteractiveLayers();
bool addedScope = false;
foreach (Transform layerRoot in _interactiveLayerRoots)
{
if (layerRoot == null || IsNavigationSkipped(layerRoot))
{
continue;
}
_holderBuffer.Clear();
layerRoot.GetComponentsInChildren(true, _holderBuffer);
for (int i = 0; i < _holderBuffer.Count; i++)
{
UIHolderObjectBase holder = _holderBuffer[i];
if (holder == null
|| holder.GetComponent<UXNavigationScope>() != null
|| IsNavigationSkipped(holder.transform))
{
continue;
}
holder.gameObject.AddComponent<UXNavigationScope>();
addedScope = true;
}
}
_holderBuffer.Clear();
if (addedScope)
{
for (int i = 0; i < _scopes.Count; i++)
{
_scopes[i]?.InvalidateSelectableCache();
}
_topScopeDirty = true;
_suppressionDirty = true;
}
}
private void EnsureWatcher(Transform target)
{
if (target == null)
{
return;
}
if (!target.TryGetComponent(out UXNavigationLayerWatcher watcher))
{
watcher = target.gameObject.AddComponent<UXNavigationLayerWatcher>();
}
watcher.Initialize(this);
} }
private UXNavigationScope FindTopScope() private UXNavigationScope FindTopScope()
{ {
UXNavigationScope bestScope = null; UXNavigationScope bestScope = null;
for (int i = 0; i < _scopes.Count; i++) for (int i = 0; i < _scopeCapacityHighWater; i++)
{ {
UXNavigationScope scope = _scopes[i]; UXNavigationScope scope = _scopes[i];
if (scope == null) if (scope == null)
@ -306,12 +253,7 @@ namespace UnityEngine.UI
_suppressionDirty = true; _suppressionDirty = true;
} }
if (!available) if (available && (bestScope == null || IsHigherPriority(scope, bestScope)))
{
continue;
}
if (bestScope == null || CompareScopePriority(scope, bestScope) < 0)
{ {
bestScope = scope; bestScope = scope;
} }
@ -320,7 +262,7 @@ namespace UnityEngine.UI
return bestScope; return bestScope;
} }
private bool IsScopeAvailable(UXNavigationScope scope) private static bool IsScopeAvailable(UXNavigationScope scope)
{ {
if (scope == null || !scope.isActiveAndEnabled || !scope.gameObject.activeInHierarchy) if (scope == null || !scope.isActiveAndEnabled || !scope.gameObject.activeInHierarchy)
{ {
@ -328,64 +270,15 @@ namespace UnityEngine.UI
} }
Canvas canvas = scope.Canvas; Canvas canvas = scope.Canvas;
if (canvas == null || canvas.gameObject.layer != UIComponent.UIShowLayer) return canvas != null
{ && canvas.gameObject.layer == UIComponent.UIShowLayer
return false; && !scope.IsNavigationSkipped
} && scope.HasAvailableSelectable();
return !scope.IsNavigationSkipped
&& scope.HasAvailableSelectable()
&& TryGetInteractiveLayerRoot(scope.transform, out _);
}
// 保留静态方法用于 DiscoverScopes 中对 LayerRoot / Holder 节点的检测
// 这些节点无 UXNavigationScope调用频次低仅在 dirty 时),无需缓存
private static bool IsNavigationSkipped(Transform current)
{
return current != null && current.GetComponentInParent<UXNavigationSkip>(true) != null;
}
private bool TryGetInteractiveLayerRoot(Transform current, out Transform layerRoot)
{
layerRoot = null;
if (current == null || _uiCanvasRoot == null)
{
return false;
}
while (current != null)
{
if (current.parent == _uiCanvasRoot)
{
layerRoot = current;
return _interactiveLayerRoots.Contains(current);
}
current = current.parent;
}
return false;
}
private bool IsHolderOwnedByScope(UIHolderObjectBase holder, UXNavigationScope scope)
{
if (holder == null || scope == null)
{
return false;
}
UXNavigationScope nearestScope = holder.GetComponent<UXNavigationScope>();
if (nearestScope == null)
{
nearestScope = holder.GetComponentInParent<UXNavigationScope>(true);
}
return nearestScope == scope;
} }
private void ApplyScopeSuppression() private void ApplyScopeSuppression()
{ {
for (int i = 0; i < _scopes.Count; i++) for (int i = 0; i < _scopeCapacityHighWater; i++)
{ {
UXNavigationScope scope = _scopes[i]; UXNavigationScope scope = _scopes[i];
if (scope == null) if (scope == null)
@ -395,30 +288,17 @@ namespace UnityEngine.UI
bool suppress = scope.IsAvailable bool suppress = scope.IsAvailable
&& _topScope != null && _topScope != null
&& !ReferenceEquals(scope, _topScope) && scope != _topScope
&& _topScope.BlockLowerScopes && _topScope.BlockLowerScopes
&& CompareScopePriority(_topScope, scope) < 0; && IsHigherPriority(_topScope, scope);
scope.SetNavigationSuppressed(suppress); scope.SetNavigationSuppressed(suppress);
} }
} }
private void EnsureGamepadSelection() private void EnsureNavigationSelection()
{ {
EventSystem eventSystem = EventSystem.current; EventSystem eventSystem = EventSystem.current;
if (eventSystem == null) if (eventSystem == null || _topScope == null || !_topScope.RequireSelectionWhenGamepad)
{
if (!_missingEventSystemLogged)
{
Debug.LogWarning("UXNavigationRuntime requires an active EventSystem for gamepad navigation.");
_missingEventSystemLogged = true;
}
return;
}
_missingEventSystemLogged = false;
if (_topScope == null || !_topScope.RequireSelectionWhenGamepad)
{ {
return; return;
} }
@ -432,78 +312,60 @@ namespace UnityEngine.UI
Selectable preferred = _topScope.GetPreferredSelectable(); Selectable preferred = _topScope.GetPreferredSelectable();
eventSystem.SetSelectedGameObject(preferred != null ? preferred.gameObject : null); eventSystem.SetSelectedGameObject(preferred != null ? preferred.gameObject : null);
_lastObservedSelection = eventSystem.currentSelectedGameObject; GameObject selectedObject = eventSystem.currentSelectedGameObject;
if (_lastObservedSelection != null) if (selectedObject != null)
{ {
_topScope.RecordSelection(_lastObservedSelection); _topScope.RecordSelection(selectedObject);
} }
} }
private void TrackSelection() private void RecordSelection(GameObject selectedObject)
{ {
EventSystem eventSystem = EventSystem.current; if (_stateDirty || _suppressionDirty)
if (eventSystem == null)
{ {
_lastObservedSelection = null; FlushStateIfDirty();
return;
} }
GameObject currentSelected = eventSystem.currentSelectedGameObject; if (_topScope != null && _topScope.IsSelectableOwnedAndValid(selectedObject))
if (ReferenceEquals(_lastObservedSelection, currentSelected))
{ {
return; _topScope.RecordSelection(selectedObject);
}
_lastObservedSelection = currentSelected;
if (_topScope != null && _topScope.IsSelectableOwnedAndValid(currentSelected))
{
_topScope.RecordSelection(currentSelected);
} }
} }
private void OnInputModeChanged(UXInputMode mode) private void OnInputModeChanged(UXInputMode mode)
{ {
EventSystem eventSystem = EventSystem.current; _cursorPolicy?.OnInputModeChanged(mode, _topScope);
if (mode == UXInputMode.Pointer) if (mode == UXInputMode.Gamepad || mode == UXInputMode.Keyboard)
{ {
if (_topScope != null) FlushStateIfDirty();
{ EnsureNavigationSelection();
Cursor.visible = true;
if (eventSystem != null)
{
eventSystem.SetSelectedGameObject(null);
}
}
_lastObservedSelection = null;
return;
} }
if (_topScope != null)
{
Cursor.visible = false;
}
EnsureGamepadSelection();
} }
private static int CompareScopePriority(UXNavigationScope left, UXNavigationScope right) private static bool IsHigherPriority(UXNavigationScope left, UXNavigationScope right)
{ {
int leftOrder = left.Canvas != null ? left.Canvas.sortingOrder : int.MinValue; int leftOrder = left.Canvas != null ? left.Canvas.sortingOrder : int.MinValue;
int rightOrder = right.Canvas != null ? right.Canvas.sortingOrder : int.MinValue; int rightOrder = right.Canvas != null ? right.Canvas.sortingOrder : int.MinValue;
int orderCompare = rightOrder.CompareTo(leftOrder); if (leftOrder != rightOrder)
if (orderCompare != 0)
{ {
return orderCompare; return leftOrder > rightOrder;
} }
int hierarchyCompare = right.GetHierarchyDepth().CompareTo(left.GetHierarchyDepth()); int leftDepth = left.GetHierarchyDepth();
if (hierarchyCompare != 0) int rightDepth = right.GetHierarchyDepth();
if (leftDepth != rightDepth)
{ {
return hierarchyCompare; return leftDepth > rightDepth;
} }
return right.ActivationSerial.CompareTo(left.ActivationSerial); return left.ActivationSerial > right.ActivationSerial;
}
private static void ReportCapacityExceeded(string message)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogError(message);
#endif
} }
} }
} }

View File

@ -1,49 +1,52 @@
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION #if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
using System.Collections.Generic;
using AlicizaX.UI.Runtime; using AlicizaX.UI.Runtime;
using UnityEngine.EventSystems;
namespace UnityEngine.UI namespace UnityEngine.UI
{ {
[DisallowMultipleComponent] [DisallowMultipleComponent]
[AddComponentMenu("UI/UX Navigation Scope")] [AddComponentMenu("UI/UX Navigation Scope")]
public sealed class UXNavigationScope : MonoBehaviour public sealed class UXNavigationScope : MonoBehaviour, ISelectHandler
{ {
private const int InvalidIndex = -1;
private const int DefaultRuntimeSelectableCapacity = 16;
[SerializeField, Header("默认选中控件")] private Selectable _defaultSelectable; [SerializeField, Header("默认选中控件")] private Selectable _defaultSelectable;
[SerializeField, Header("编辑器烘焙导航控件")] private Selectable[] _bakedSelectables = System.Array.Empty<Selectable>();
[SerializeField, Header("显式导航控件列表")] private List<Selectable> _explicitSelectables = new(); [SerializeField, Header("动态控件容量")] private int _runtimeSelectableCapacity = DefaultRuntimeSelectableCapacity;
[SerializeField, Header("记住上次选中")] private bool _rememberLastSelection = true; [SerializeField, Header("记住上次选中")] private bool _rememberLastSelection = true;
[SerializeField, Header("手柄/键盘模式必须有选中")] private bool _requireSelectionWhenGamepad = true;
[SerializeField, Header("手柄模式必须有选中")] private bool _requireSelectionWhenGamepad = true;
[SerializeField, Header("阻断下层导航域")] private bool _blockLowerScopes = true; [SerializeField, Header("阻断下层导航域")] private bool _blockLowerScopes = true;
[SerializeField, Header("自动选中首个可用控件")] private bool _autoSelectFirstAvailable = true; [SerializeField, Header("自动选中首个可用控件")] private bool _autoSelectFirstAvailable = true;
private readonly List<Selectable> _cachedSelectables = new(16); private Selectable[] _runtimeSelectables;
private readonly HashSet<Selectable> _cachedSelectableSet = new(); private Navigation[] _bakedBaselineNavigation;
private readonly Dictionary<Selectable, Navigation> _baselineNavigation = new(); private Navigation[] _runtimeBaselineNavigation;
private readonly List<Selectable> _removeBuffer = new(8);
private readonly List<Selectable> _selectableScanBuffer = new(16);
private Canvas _cachedCanvas; private Canvas _cachedCanvas;
private UIHolderObjectBase _cachedHolder; private UIHolderObjectBase _cachedHolder;
private Selectable _lastSelected; private Selectable _lastSelected;
private bool _cacheDirty = true;
private bool _navigationSuppressed; private bool _navigationSuppressed;
private int _cachedHierarchyDepth = -1; private int _cachedHierarchyDepth = -1;
private bool _cachedIsSkipped; private bool _cachedIsSkipped;
private bool _isSkippedCacheValid; private bool _isSkippedCacheValid;
private int _runtimeSelectableCount;
internal int RuntimeIndex { get; set; } = InvalidIndex;
internal ulong ActivationSerial { get; set; } internal ulong ActivationSerial { get; set; }
internal bool IsAvailable { get; set; } internal bool IsAvailable { get; set; }
internal bool WasAvailable { get; set; } internal bool WasAvailable { get; set; }
public bool NavigationSuppressed => _navigationSuppressed;
internal int BakedSelectableCount => _bakedSelectables != null ? _bakedSelectables.Length : 0;
public int RuntimeSelectableCount => _runtimeSelectableCount;
public Selectable DefaultSelectable public Selectable DefaultSelectable
{ {
get => _defaultSelectable; get => _defaultSelectable;
set => _defaultSelectable = value; set
{
_defaultSelectable = value;
MarkRuntimeStateDirty();
}
} }
public bool RememberLastSelection => _rememberLastSelection; public bool RememberLastSelection => _rememberLastSelection;
@ -59,6 +62,7 @@ namespace UnityEngine.UI
_cachedIsSkipped = GetComponentInParent<UXNavigationSkip>(true) != null; _cachedIsSkipped = GetComponentInParent<UXNavigationSkip>(true) != null;
_isSkippedCacheValid = true; _isSkippedCacheValid = true;
} }
return _cachedIsSkipped; return _cachedIsSkipped;
} }
} }
@ -93,9 +97,16 @@ namespace UnityEngine.UI
} }
} }
private void Awake()
{
EnsureRuntimeBuffers();
CaptureAllBaselines();
}
private void OnEnable() private void OnEnable()
{ {
_cacheDirty = true; EnsureRuntimeBuffers();
CaptureAllBaselines();
UXNavigationRuntime.EnsureInstance().RegisterScope(this); UXNavigationRuntime.EnsureInstance().RegisterScope(this);
} }
@ -115,25 +126,108 @@ namespace UnityEngine.UI
private void OnTransformChildrenChanged() private void OnTransformChildrenChanged()
{ {
_cacheDirty = true; MarkRuntimeStateDirty();
} }
private void OnTransformParentChanged() private void OnTransformParentChanged()
{ {
_cacheDirty = true;
_cachedCanvas = null; _cachedCanvas = null;
_cachedHolder = null; _cachedHolder = null;
_cachedHierarchyDepth = -1; _cachedHierarchyDepth = -1;
_isSkippedCacheValid = false; _isSkippedCacheValid = false;
MarkRuntimeStateDirty();
} }
#if UNITY_EDITOR #if UNITY_EDITOR
private void OnValidate() private void OnValidate()
{ {
_cacheDirty = true; if (_runtimeSelectableCapacity < 0)
{
_runtimeSelectableCapacity = 0;
}
} }
#endif #endif
public bool RegisterSelectable(Selectable selectable)
{
if (selectable == null || !Owns(selectable.gameObject) || ContainsSelectable(selectable))
{
return false;
}
EnsureRuntimeBuffers();
if (_runtimeSelectableCount >= _runtimeSelectables.Length)
{
ReportCapacityExceeded();
return false;
}
_runtimeSelectables[_runtimeSelectableCount] = selectable;
_runtimeBaselineNavigation[_runtimeSelectableCount] = selectable.navigation;
_runtimeSelectableCount++;
if (_navigationSuppressed)
{
SetSelectableSuppressed(selectable, true);
}
MarkRuntimeStateDirty();
return true;
}
public bool UnregisterSelectable(Selectable selectable)
{
if (selectable == null || _runtimeSelectables == null)
{
return false;
}
for (int i = 0; i < _runtimeSelectableCount; i++)
{
if (_runtimeSelectables[i] != selectable)
{
continue;
}
if (_navigationSuppressed)
{
selectable.navigation = _runtimeBaselineNavigation[i];
}
int last = _runtimeSelectableCount - 1;
_runtimeSelectables[i] = _runtimeSelectables[last];
_runtimeBaselineNavigation[i] = _runtimeBaselineNavigation[last];
_runtimeSelectables[last] = null;
_runtimeBaselineNavigation[last] = default(Navigation);
_runtimeSelectableCount--;
if (_lastSelected == selectable)
{
_lastSelected = null;
}
MarkRuntimeStateDirty();
return true;
}
return false;
}
public void InvalidateSelectableCache()
{
CaptureAllBaselines();
MarkRuntimeStateDirty();
}
public void OnSelect(BaseEventData eventData)
{
GameObject selectedObject = eventData != null ? eventData.selectedObject : null;
UXNavigationRuntime.NotifySelection(selectedObject);
}
internal void InvalidateSkipCacheOnly()
{
_isSkippedCacheValid = false;
}
internal int GetHierarchyDepth() internal int GetHierarchyDepth()
{ {
if (_cachedHierarchyDepth >= 0) if (_cachedHierarchyDepth >= 0)
@ -160,14 +254,12 @@ namespace UnityEngine.UI
return false; return false;
} }
var nearestScope = target.GetComponentInParent<UXNavigationScope>(true); UXNavigationScope nearestScope = target.GetComponentInParent<UXNavigationScope>(true);
return nearestScope == this; return nearestScope == this;
} }
internal Selectable GetPreferredSelectable() internal Selectable GetPreferredSelectable()
{ {
RefreshSelectableCache();
if (_rememberLastSelection && IsSelectableValid(_lastSelected)) if (_rememberLastSelection && IsSelectableValid(_lastSelected))
{ {
return _lastSelected; return _lastSelected;
@ -183,36 +275,20 @@ namespace UnityEngine.UI
return null; return null;
} }
for (int i = 0; i < _cachedSelectables.Count; i++) Selectable selectable = FirstUsable(_bakedSelectables, BakedSelectableCount);
if (selectable != null)
{ {
Selectable selectable = _cachedSelectables[i]; return selectable;
if (IsSelectableUsable(selectable))
{
return selectable;
}
} }
return null; return FirstUsable(_runtimeSelectables, _runtimeSelectableCount);
} }
internal bool HasAvailableSelectable() internal bool HasAvailableSelectable()
{ {
RefreshSelectableCache(); return IsSelectableValid(_defaultSelectable)
|| FirstUsable(_bakedSelectables, BakedSelectableCount) != null
if (IsSelectableValid(_defaultSelectable)) || FirstUsable(_runtimeSelectables, _runtimeSelectableCount) != null;
{
return true;
}
for (int i = 0; i < _cachedSelectables.Count; i++)
{
if (IsSelectableUsable(_cachedSelectables[i]))
{
return true;
}
}
return false;
} }
internal void RecordSelection(GameObject selectedObject) internal void RecordSelection(GameObject selectedObject)
@ -241,109 +317,142 @@ namespace UnityEngine.UI
return; return;
} }
RefreshSelectableCache(); CaptureAllBaselines();
_navigationSuppressed = suppressed; _navigationSuppressed = suppressed;
for (int i = 0; i < _cachedSelectables.Count; i++) ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, suppressed);
ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, suppressed);
}
private void EnsureRuntimeBuffers()
{
int capacity = _runtimeSelectableCapacity > 0 ? _runtimeSelectableCapacity : 0;
if (_runtimeSelectables == null || _runtimeSelectables.Length != capacity)
{ {
Selectable selectable = _cachedSelectables[i]; _runtimeSelectables = capacity > 0 ? new Selectable[capacity] : System.Array.Empty<Selectable>();
_runtimeBaselineNavigation = capacity > 0 ? new Navigation[capacity] : System.Array.Empty<Navigation>();
_runtimeSelectableCount = 0;
}
int bakedCount = BakedSelectableCount;
if (_bakedBaselineNavigation == null || _bakedBaselineNavigation.Length != bakedCount)
{
_bakedBaselineNavigation = bakedCount > 0 ? new Navigation[bakedCount] : System.Array.Empty<Navigation>();
}
}
private void CaptureAllBaselines()
{
EnsureRuntimeBuffers();
CaptureBaseline(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount);
CaptureBaseline(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount);
}
private static void CaptureBaseline(Selectable[] selectables, Navigation[] baseline, int count)
{
if (selectables == null || baseline == null)
{
return;
}
for (int i = 0; i < count; i++)
{
Selectable selectable = selectables[i];
if (selectable != null)
{
baseline[i] = selectable.navigation;
}
}
}
private static void ApplySuppression(Selectable[] selectables, Navigation[] baseline, int count, bool suppressed)
{
if (selectables == null || baseline == null)
{
return;
}
for (int i = 0; i < count; i++)
{
Selectable selectable = selectables[i];
if (selectable == null) if (selectable == null)
{ {
continue; continue;
} }
if (!_baselineNavigation.ContainsKey(selectable))
{
_baselineNavigation[selectable] = selectable.navigation;
}
if (suppressed) if (suppressed)
{ {
Navigation navigation = selectable.navigation; SetSelectableSuppressed(selectable, true);
navigation.mode = Navigation.Mode.None;
selectable.navigation = navigation;
} }
else if (_baselineNavigation.TryGetValue(selectable, out Navigation navigation)) else
{ {
selectable.navigation = navigation; selectable.navigation = baseline[i];
} }
} }
} }
internal void RefreshSelectableCache() private static void SetSelectableSuppressed(Selectable selectable, bool suppressed)
{ {
if (!_cacheDirty) if (selectable == null || !suppressed)
{ {
return; return;
} }
_cacheDirty = false; Navigation navigation = selectable.navigation;
_cachedSelectables.Clear(); navigation.mode = Navigation.Mode.None;
_cachedSelectableSet.Clear(); selectable.navigation = navigation;
if (_explicitSelectables != null && _explicitSelectables.Count > 0)
{
for (int i = 0; i < _explicitSelectables.Count; i++)
{
TryAddSelectable(_explicitSelectables[i]);
}
}
else
{
_selectableScanBuffer.Clear();
GetComponentsInChildren(true, _selectableScanBuffer);
for (int i = 0; i < _selectableScanBuffer.Count; i++)
{
TryAddSelectable(_selectableScanBuffer[i]);
}
}
_removeBuffer.Clear();
foreach (Selectable key in _baselineNavigation.Keys)
{
if (!_cachedSelectableSet.Contains(key))
{
_removeBuffer.Add(key);
}
}
for (int i = 0; i < _removeBuffer.Count; i++)
{
_baselineNavigation.Remove(_removeBuffer[i]);
}
_selectableScanBuffer.Clear();
} }
public void InvalidateSelectableCache() private bool ContainsSelectable(Selectable selectable)
{ {
_cacheDirty = true; return IndexOf(_bakedSelectables, BakedSelectableCount, selectable) >= 0
} || IndexOf(_runtimeSelectables, _runtimeSelectableCount, selectable) >= 0;
private void TryAddSelectable(Selectable selectable)
{
if (selectable == null || !Owns(selectable.gameObject) || !_cachedSelectableSet.Add(selectable))
{
return;
}
_cachedSelectables.Add(selectable);
if (!_baselineNavigation.ContainsKey(selectable) || !_navigationSuppressed)
{
_baselineNavigation[selectable] = selectable.navigation;
}
} }
private bool IsSelectableValid(Selectable selectable) private bool IsSelectableValid(Selectable selectable)
{ {
return IsSelectableUsable(selectable) return IsSelectableUsable(selectable) && ContainsSelectable(selectable);
&& (_cachedSelectableSet.Contains(selectable) || Owns(selectable.gameObject)); }
private static Selectable FirstUsable(Selectable[] selectables, int count)
{
if (selectables == null)
{
return null;
}
for (int i = 0; i < count; i++)
{
Selectable selectable = selectables[i];
if (IsSelectableUsable(selectable))
{
return selectable;
}
}
return null;
}
private static int IndexOf(Selectable[] selectables, int count, Selectable selectable)
{
if (selectables == null || selectable == null)
{
return InvalidIndex;
}
for (int i = 0; i < count; i++)
{
if (selectables[i] == selectable)
{
return i;
}
}
return InvalidIndex;
} }
private static bool IsSelectableUsable(Selectable selectable) private static bool IsSelectableUsable(Selectable selectable)
{ {
return selectable != null return selectable != null && selectable.IsActive() && selectable.IsInteractable();
&& selectable.IsActive()
&& selectable.IsInteractable();
} }
private static Selectable GetSelectableFromObject(GameObject selectedObject) private static Selectable GetSelectableFromObject(GameObject selectedObject)
@ -357,6 +466,21 @@ namespace UnityEngine.UI
? selectable ? selectable
: selectedObject.GetComponentInParent<Selectable>(); : selectedObject.GetComponentInParent<Selectable>();
} }
private void MarkRuntimeStateDirty()
{
if (UXNavigationRuntime.TryGetInstance(out var runtime))
{
runtime.MarkStateDirty();
}
}
private static void ReportCapacityExceeded()
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogError("UXNavigationScope runtime selectable capacity exceeded.");
#endif
}
} }
} }
#endif #endif

View File

@ -5,6 +5,28 @@ namespace UnityEngine.UI
[AddComponentMenu("UI/UX Navigation Skip")] [AddComponentMenu("UI/UX Navigation Skip")]
public sealed class UXNavigationSkip : MonoBehaviour public sealed class UXNavigationSkip : MonoBehaviour
{ {
private void OnEnable()
{
InvalidateNavigation();
}
private void OnDisable()
{
InvalidateNavigation();
}
private void OnTransformParentChanged()
{
InvalidateNavigation();
}
private static void InvalidateNavigation()
{
if (UXNavigationRuntime.TryGetInstance(out var runtime))
{
runtime.InvalidateSkipCaches();
}
}
} }
} }
#endif #endif