优化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))]
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 _entriesProp;
private readonly Dictionary<int, bool> _foldouts = new Dictionary<int, bool>();
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()
{
_controllerProp = serializedObject.FindProperty("_controller");
_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()
{
serializedObject.Update();
EnsureStyles();
var binding = (UXBinding)target;
DrawHeader(binding);
EditorGUILayout.Space(6f);
DrawControllerField(binding);
EditorGUILayout.Space(6f);
DrawEntries(binding);
serializedObject.ApplyModifiedProperties();
@ -37,39 +148,11 @@ namespace UnityEngine.UI
private void DrawHeader(UXBinding binding)
{
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();
if (GUILayout.Button("Add Rule"))
{
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);
EditorGUILayout.LabelField("UX Binding", EditorStyles.boldLabel, GUILayout.Width(82f));
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())
{
Undo.RecordObject(binding, "Change UX Binding Controller");
@ -78,13 +161,40 @@ namespace UnityEngine.UI
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)
{
EditorGUILayout.HelpBox("Assign a UXController on this object or one of its parents.", MessageType.Warning);
}
else
{
EditorGUILayout.LabelField($"Bound To: {binding.Controller.name}", EditorStyles.miniLabel);
EditorGUILayout.HelpBox("Assign a UXController or use Auto Bind.", MessageType.Warning);
}
EditorGUILayout.EndVertical();
@ -112,27 +222,39 @@ namespace UnityEngine.UI
SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index);
SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId");
SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex");
SerializedProperty controllerIndexMaskProp = entryProp.FindPropertyRelative("_controllerIndexMask");
SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property");
SerializedProperty valueProp = entryProp.FindPropertyRelative("_value");
SerializedProperty indexedValuesProp = entryProp.FindPropertyRelative("_indexedValues");
SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode");
SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue");
UXBindingProperty property = (UXBindingProperty)propertyProp.enumValueIndex;
bool expanded = _foldouts.ContainsKey(index) && _foldouts[index];
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.BeginHorizontal();
expanded = EditorGUILayout.Foldout(expanded, $"[{index}] {label}", true);
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);
SceneView.RepaintAll();
MoveEntry(index, index - 1);
}
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);
}
@ -141,39 +263,75 @@ namespace UnityEngine.UI
if (expanded)
{
DrawControllerSelector(binding, controllerIdProp, controllerIndexProp);
DrawPropertySelector(entryProp, binding.gameObject, propertyProp);
property = (UXBindingProperty)propertyProp.enumValueIndex;
UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(property);
bool indexChanged = DrawControllerSelector(binding, controllerIdProp, controllerIndexProp, controllerIndexMaskProp, property == UXBindingProperty.GameObjectActive);
EditorGUILayout.Space(2f);
EditorGUILayout.LabelField("Value", EditorStyles.boldLabel);
DrawValueField(valueProp, metadata, "Matched Value");
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Use Current"))
if (property == UXBindingProperty.GameObjectActive)
{
binding.CaptureEntryValue(index);
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"))
DrawGameObjectActiveHint(controllerIndexMaskProp.intValue);
ForceGameObjectActiveValues(valueProp, fallbackModeProp, fallbackValueProp);
if (indexChanged)
{
binding.CaptureEntryFallbackValue(index);
serializedObject.ApplyModifiedProperties();
binding.ApplyEntryValue(index, GetFirstSelectedIndex(controllerIndexMaskProp.intValue));
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);
}
@ -182,22 +340,25 @@ namespace UnityEngine.UI
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;
if (controller == null || controller.Controllers.Count == 0)
{
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;
for (int i = 0; i < controller.Controllers.Count; i++)
{
UXController.ControllerDefinition definition = controller.Controllers[i];
names[i] = definition.Name;
_controllerNames[i] = definition.Name;
if (definition.Id == controllerIdProp.stringValue)
{
selectedController = i;
@ -205,59 +366,23 @@ namespace UnityEngine.UI
}
EditorGUI.BeginChangeCheck();
selectedController = EditorGUILayout.Popup("Controller", selectedController, names);
selectedController = EditorGUILayout.Popup(selectedController, _controllerNames, GUILayout.MinWidth(90f));
if (EditorGUI.EndChangeCheck())
{
controllerIdProp.stringValue = controller.Controllers[selectedController].Id;
controllerIndexProp.intValue = 0;
controllerIndexMaskProp.intValue = 1;
changed = true;
}
UXController.ControllerDefinition selectedDefinition = controller.Controllers[selectedController];
int maxIndex = Mathf.Max(1, selectedDefinition.Length);
controllerIndexProp.intValue = Mathf.Clamp(controllerIndexProp.intValue, 0, maxIndex - 1);
controllerIndexMaskProp.intValue = ClampMask(controllerIndexMaskProp.intValue, maxIndex, controllerIndexProp.intValue);
string[] indexNames = new string[maxIndex];
for (int i = 0; i < maxIndex; i++)
{
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);
}
changed |= DrawIndexMask(controllerIndexMaskProp, controllerIndexProp, maxIndex, multiSelect);
EditorGUILayout.EndHorizontal();
return changed;
}
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;
_entriesProp.InsertArrayElementAtIndex(index);
@ -322,31 +447,73 @@ namespace UnityEngine.UI
SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index);
SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId");
SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex");
SerializedProperty controllerIndexMaskProp = entryProp.FindPropertyRelative("_controllerIndexMask");
SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property");
SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode");
SerializedProperty valueProp = entryProp.FindPropertyRelative("_value");
SerializedProperty indexedValuesProp = entryProp.FindPropertyRelative("_indexedValues");
SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue");
SerializedProperty capturedDefaultProp = entryProp.FindPropertyRelative("_capturedDefault");
SerializedProperty hasCapturedDefaultProp = entryProp.FindPropertyRelative("_hasCapturedDefault");
SerializedProperty capturedPropertyProp = entryProp.FindPropertyRelative("_capturedProperty");
controllerIdProp.stringValue = string.Empty;
controllerIdProp.stringValue = controllerId;
controllerIndexProp.intValue = 0;
propertyProp.enumValueIndex = (int)UXBindingProperty.GameObjectActive;
controllerIndexMaskProp.intValue = 1;
propertyProp.enumValueIndex = (int)property;
fallbackModeProp.enumValueIndex = (int)UXBindingFallbackMode.RestoreCapturedDefault;
ResetValue(valueProp);
indexedValuesProp.ClearArray();
ResetValue(fallbackValueProp);
ResetValue(capturedDefaultProp);
hasCapturedDefaultProp.boolValue = false;
capturedPropertyProp.enumValueIndex = (int)UXBindingProperty.GameObjectActive;
ApplyDefaultFallbackForProperty(entryProp, UXBindingProperty.GameObjectActive);
if (binding.Controller != null && binding.Controller.Controllers.Count > 0)
{
controllerIdProp.stringValue = binding.Controller.Controllers[0].Id;
}
capturedPropertyProp.enumValueIndex = (int)property;
ApplyDefaultFallbackForProperty(entryProp, property);
_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)
@ -367,6 +534,15 @@ namespace UnityEngine.UI
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");
_entriesProp.DeleteArrayElementAtIndex(index);
CleanupFoldouts(index);
@ -375,6 +551,22 @@ namespace UnityEngine.UI
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)
{
_foldouts.Remove(removedIndex);
@ -411,6 +603,230 @@ namespace UnityEngine.UI
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

View File

@ -77,6 +77,7 @@ namespace UnityEngine.UI
SerializedProperty idProp = entryProp.FindPropertyRelative("_id");
SerializedProperty nameProp = entryProp.FindPropertyRelative("_name");
SerializedProperty lengthProp = entryProp.FindPropertyRelative("_length");
SerializedProperty defaultIndexProp = entryProp.FindPropertyRelative("_defaultIndex");
SerializedProperty descriptionProp = entryProp.FindPropertyRelative("_description");
bool expanded = _foldouts.ContainsKey(index) && _foldouts[index];
@ -110,6 +111,7 @@ namespace UnityEngine.UI
{
EditorGUILayout.PropertyField(nameProp, new GUIContent("Name"));
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"));
EditorGUI.BeginDisabledGroup(true);
@ -181,6 +183,7 @@ namespace UnityEngine.UI
entryProp.FindPropertyRelative("_id").stringValue = string.Empty;
entryProp.FindPropertyRelative("_name").stringValue = $"Controller {index + 1}";
entryProp.FindPropertyRelative("_length").intValue = 2;
entryProp.FindPropertyRelative("_defaultIndex").intValue = 0;
entryProp.FindPropertyRelative("_description").stringValue = string.Empty;
_foldouts[index] = true;
@ -195,6 +198,15 @@ namespace UnityEngine.UI
SerializedProperty entryProp = _controllersProp.GetArrayElementAtIndex(index);
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");
_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

View File

@ -8,29 +8,39 @@ namespace UnityEngine.UI
public sealed class UXControllerSceneOverlayWindow : EditorWindow
{
private const string OverlayEnabledKey = "AlicizaX.UI.UXControllerSceneOverlay.Enabled";
private const string OverlayAutoFocusKey = "AlicizaX.UI.UXControllerSceneOverlay.AutoFocus";
private const float OverlayWidth = 360f;
private const float OverlayMargin = 12f;
private const string MenuPath = "Window/UX/Controller Scene Overlay";
private const float OverlayWidth = 340f;
private const float OverlayMargin = 10f;
private static bool s_overlayEnabled;
private static bool s_autoFocusSceneView;
private static Vector2 s_scrollPosition;
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")]
public static void ShowWindow()
[MenuItem(MenuPath)]
public static void ToggleOverlay()
{
var window = GetWindow<UXControllerSceneOverlayWindow>("UX Controller Overlay");
window.minSize = new Vector2(320f, 140f);
window.Show();
s_overlayEnabled = !s_overlayEnabled;
EditorPrefs.SetBool(OverlayEnabledKey, s_overlayEnabled);
Menu.SetChecked(MenuPath, s_overlayEnabled);
EnsureRegistered();
SceneView.RepaintAll();
}
[MenuItem(MenuPath, true)]
private static bool ToggleOverlayValidate()
{
Menu.SetChecked(MenuPath, s_overlayEnabled);
return true;
}
[InitializeOnLoadMethod]
private static void Initialize()
{
s_overlayEnabled = EditorPrefs.GetBool(OverlayEnabledKey, false);
s_autoFocusSceneView = EditorPrefs.GetBool(OverlayAutoFocusKey, false);
Menu.SetChecked(MenuPath, s_overlayEnabled);
EnsureRegistered();
}
@ -45,37 +55,6 @@ namespace UnityEngine.UI
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)
{
if (!s_overlayEnabled)
@ -83,51 +62,44 @@ namespace UnityEngine.UI
return;
}
if (s_autoFocusSceneView && sceneView != null)
{
s_autoFocusSceneView = false;
EditorPrefs.SetBool(OverlayAutoFocusKey, false);
sceneView.Focus();
}
List<UXController> controllers = GetSceneControllers();
GetSceneControllers(Controllers);
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(
sceneView.position.width - OverlayWidth - OverlayMargin,
OverlayMargin,
OverlayWidth,
height);
GUILayout.BeginArea(area, EditorStyles.helpBox);
DrawOverlayContent(controllers);
GUILayout.BeginArea(area, GUI.skin.window);
DrawOverlayContent(Controllers);
GUILayout.EndArea();
Handles.EndGUI();
}
private static void DrawOverlayContent(List<UXController> controllers)
{
EnsureStyles();
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();
if (GUILayout.Button("Hide", GUILayout.Width(64f)))
if (GUILayout.Button("X", EditorStyles.miniButton, GUILayout.Width(22f)))
{
s_overlayEnabled = false;
EditorPrefs.SetBool(OverlayEnabledKey, s_overlayEnabled);
SceneView.RepaintAll();
}
if (GUILayout.Button("Refresh", GUILayout.Width(64f)))
{
Menu.SetChecked(MenuPath, false);
SceneView.RepaintAll();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4f);
EditorGUILayout.Space(2f);
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;
}
@ -152,16 +124,17 @@ namespace UnityEngine.UI
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
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();
if (GUILayout.Button("Select", GUILayout.Width(52f)))
if (GUILayout.Button("Ping", EditorStyles.miniButtonLeft, GUILayout.Width(42f)))
{
Selection.activeGameObject = controller.gameObject;
EditorGUIUtility.PingObject(controller.gameObject);
}
if (GUILayout.Button("Reset", GUILayout.Width(52f)))
if (GUILayout.Button("Reset", EditorStyles.miniButtonRight, GUILayout.Width(44f)))
{
controller.ResetAllControllers();
EditorUtility.SetDirty(controller);
@ -170,7 +143,6 @@ namespace UnityEngine.UI
EditorGUILayout.EndHorizontal();
EditorGUILayout.LabelField(GetHierarchyPath(controller.transform), EditorStyles.miniLabel);
EditorGUILayout.LabelField($"Bindings: {controller.Bindings.Count}", EditorStyles.miniLabel);
for (int controllerIndex = 0; controllerIndex < controller.ControllerCount; controllerIndex++)
{
@ -188,38 +160,33 @@ namespace UnityEngine.UI
private static void DrawDefinitionPreview(UXController controller, UXController.ControllerDefinition definition)
{
EditorGUILayout.Space(3f);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField(definition.Name, EditorStyles.boldLabel);
int currentIndex = Mathf.Max(0, definition.SelectedIndex);
int length = Mathf.Max(1, definition.Length);
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))
{
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>();
for (int i = 0; i < allControllers.Length; i++)
@ -248,8 +215,35 @@ namespace UnityEngine.UI
results.Add(controller);
}
results.Sort((left, right) => string.CompareOrdinal(GetHierarchyPath(left.transform), GetHierarchyPath(right.transform)));
return results;
results.Sort(CompareControllers);
}
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)

View File

@ -1,8 +1,7 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
namespace UnityEditor.UI
{
@ -12,23 +11,23 @@ namespace UnityEditor.UI
private SerializedProperty m_Toggles;
private SerializedProperty m_AllowSwitchOff;
private SerializedProperty m_DefaultToggle;
private UXGroup _target;
private ReorderableList _reorderableList;
private UXGroup m_Target;
private ReorderableList m_ReorderableList;
private void OnEnable()
{
_target = (UXGroup)target;
m_Target = (UXGroup)target;
m_Toggles = serializedObject.FindProperty("m_Toggles");
m_AllowSwitchOff = serializedObject.FindProperty("m_AllowSwitchOff");
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,
drawElementCallback = DrawElement,
onAddCallback = OnAddList,
onRemoveCallback = OnRemoveList,
onChangedCallback = OnChanged,
displayAdd = false,
onChangedCallback = OnChanged
};
}
@ -37,160 +36,166 @@ namespace UnityEditor.UI
serializedObject.Update();
EditorGUILayout.PropertyField(m_AllowSwitchOff);
// Default selector: only show toggles that are currently in the group's m_Toggles list
DrawDefaultToggleSelector();
DrawTools();
bool isPlaying = Application.isPlaying || EditorApplication.isPlaying;
m_ReorderableList.draggable = !isPlaying;
m_ReorderableList.displayAdd = !isPlaying;
m_ReorderableList.displayRemove = !isPlaying;
_reorderableList.draggable = !isPlaying;
_reorderableList.displayAdd = !isPlaying;
_reorderableList.displayRemove = !isPlaying;
bool prevEnabled = GUI.enabled;
if (isPlaying) GUI.enabled = false;
_reorderableList.DoLayoutList();
GUI.enabled = prevEnabled;
bool previousEnabled = GUI.enabled;
GUI.enabled = !isPlaying;
m_ReorderableList.DoLayoutList();
GUI.enabled = previousEnabled;
serializedObject.ApplyModifiedProperties();
// 在编辑器下尽量实时同步状态
if (!Application.isPlaying)
{
// 保证序列化数据写入后同步 group 状态
_target.EnsureValidState();
}
m_Target.EnsureValidState();
}
private void DrawDefaultToggleSelector()
{
// Build a list of current toggles (non-null)
List<UXToggle> toggles = new List<UXToggle>();
int toggleCount = CountValidToggles();
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++)
{
var elem = m_Toggles.GetArrayElementAtIndex(i);
var t = elem.objectReferenceValue as UXToggle;
if (t != null)
toggles.Add(t);
UXToggle toggle = m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue as UXToggle;
if (toggle == null)
continue;
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;
int currentIndex = 0;
if (currentDefault != null)
int currentIndex = requireDefault ? -1 : 0;
for (int i = 0; i < toggles.Length; i++)
{
int found = toggles.IndexOf(currentDefault);
if (found >= 0)
currentIndex = found + 1; // +1 because 0 is <None>
else
if (toggles[i] == currentDefault)
{
// Current default is not in the list -> clear it
m_DefaultToggle.objectReferenceValue = null;
serializedObject.ApplyModifiedProperties();
currentIndex = 0;
currentIndex = i + optionOffset;
break;
}
}
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();
int newIndex = EditorGUILayout.Popup("Default Toggle", currentIndex, options);
if (EditorGUI.EndChangeCheck())
{
UXToggle newDefault = null;
if (newIndex > 0)
newDefault = toggles[newIndex - 1];
UXToggle newDefault = newIndex >= optionOffset ? toggles[newIndex - optionOffset] : null;
m_DefaultToggle.objectReferenceValue = newDefault;
serializedObject.ApplyModifiedProperties();
// 如果选择了一个非空默认项,则在 allowSwitchOff == false 时将其设为选中状态(并通知组)
if (newDefault != null)
{
// 确保该 toggle 在 group 中(理论上应该如此)
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 中被处理
AssignGroup(newDefault, m_Target);
}
}
}
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)
{
EditorGUI.LabelField(rect, "Toggles", EditorStyles.boldLabel);
}
// 记录旧的引用用于侦测变化
private UXToggle previousRef;
private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
SerializedProperty element = m_Toggles.GetArrayElementAtIndex(index);
UXToggle oldToggle = element.objectReferenceValue as UXToggle;
rect.y += 2;
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;
string label = $"[{index}] {(oldButton != null ? oldButton.name : "Null")}";
bool duplicate = oldToggle != null && HasDuplicate(oldToggle, index);
bool wrongGroup = oldToggle != null && oldToggle.group != null && oldToggle.group != m_Target;
if (duplicate || wrongGroup)
EditorGUI.DrawRect(fieldRect, new Color(1f, 0.55f, 0f, 0.2f));
EditorGUI.BeginChangeCheck();
var newRef = EditorGUI.ObjectField(fieldRect, label, oldButton, typeof(UXToggle), true) as UXToggle;
UXToggle newToggle = EditorGUI.ObjectField(fieldRect, label, oldToggle, typeof(UXToggle), true) as UXToggle;
if (EditorGUI.EndChangeCheck())
{
// 先处理 Remove旧值存在且不同
if (oldButton != null && oldButton != newRef)
{
OnRemove(oldButton);
}
if (oldToggle != null && oldToggle != newToggle)
AssignGroup(oldToggle, null);
// 再处理 Add新值非空
if (newRef != null && oldButton != newRef)
{
OnAdd(newRef);
}
if (newToggle != null && oldToggle != newToggle)
AssignGroup(newToggle, m_Target);
// 最后把引用写回去
element.objectReferenceValue = newRef;
element.objectReferenceValue = newToggle;
serializedObject.ApplyModifiedProperties();
m_Target.EnsureValidState();
}
}
private void OnAddList(ReorderableList list)
{
int newIndex = m_Toggles.arraySize;
m_Toggles.arraySize++;
serializedObject.ApplyModifiedProperties();
var newElem = m_Toggles.GetArrayElementAtIndex(newIndex);
newElem.objectReferenceValue = null;
m_Toggles.GetArrayElementAtIndex(m_Toggles.arraySize - 1).objectReferenceValue = null;
serializedObject.ApplyModifiedProperties();
}
@ -199,72 +204,122 @@ namespace UnityEditor.UI
if (list.index < 0 || list.index >= m_Toggles.arraySize)
return;
var oldButton = m_Toggles.GetArrayElementAtIndex(list.index).objectReferenceValue as UXToggle;
if (oldButton)
{
OnRemove(oldButton);
}
UXToggle oldToggle = m_Toggles.GetArrayElementAtIndex(list.index).objectReferenceValue as UXToggle;
if (oldToggle != null)
AssignGroup(oldToggle, null);
m_Toggles.DeleteArrayElementAtIndex(list.index);
serializedObject.ApplyModifiedProperties();
m_Target.EnsureValidState();
}
private void OnChanged(ReorderableList list)
{
serializedObject.ApplyModifiedProperties();
// 编辑器变动后同步 group 状态和默认项
if (!Application.isPlaying)
_target.EnsureValidState();
m_Target.EnsureValidState();
}
// ========================
// 自动调用的新增方法
// ========================
private void OnAdd(UXToggle toggle)
private void CollectChildren()
{
if (toggle == null)
return;
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)
UXToggle[] toggles = m_Target.GetComponentsInChildren<UXToggle>(true);
m_Toggles.arraySize = toggles.Length;
for (int i = 0; i < toggles.Length; i++)
{
m_DefaultToggle.objectReferenceValue = null;
serializedObject.ApplyModifiedProperties();
m_Toggles.GetArrayElementAtIndex(i).objectReferenceValue = toggles[i];
AssignGroup(toggles[i], m_Target);
}
if (!Application.isPlaying && _target != null)
_target.EnsureValidState();
serializedObject.ApplyModifiedProperties();
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
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 _hotkeyPressType;
private SerializedProperty _component;
private SerializedProperty _holder;
private void OnEnable()
{
_component = serializedObject.FindProperty("_component");
_holder = serializedObject.FindProperty("_holder");
_hotkeyAction = serializedObject.FindProperty("_hotkeyAction");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
}
@ -30,7 +32,7 @@ namespace UnityEditor.UI
MessageType.Info
);
if (hotkeyComponent.GetComponentInParent<UIHolderObjectBase>(true) == null)
if (_holder.objectReferenceValue == null)
{
EditorGUILayout.HelpBox(
"No UIHolderObjectBase was found in parents. This hotkey will not register at runtime.",
@ -46,6 +48,16 @@ namespace UnityEditor.UI
_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))
{
@ -53,6 +65,7 @@ namespace UnityEditor.UI
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.PropertyField(_component, new GUIContent("Component"));
EditorGUILayout.PropertyField(_holder, new GUIContent("Holder"));
EditorGUI.EndDisabledGroup();
EditorGUILayout.PropertyField(_hotkeyAction, new GUIContent("Input Action"));

View File

@ -4,7 +4,6 @@ using UnityEditor;
using UnityEngine;
using System.IO;
using UnityEditorInternal;
using System.Linq;
using UnityEditor.AnimatedValues;
namespace UnityEngine.UI
@ -289,15 +288,6 @@ namespace UnityEngine.UI
if (EditorGUI.EndChangeCheck())
{
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();
@ -399,15 +389,7 @@ namespace UnityEngine.UI
protected void TypeGUI()
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_Type, m_SpriteTypeContent);
if (EditorGUI.EndChangeCheck())
{
if (m_Type.intValue != 0 && m_ColorType.intValue == 1)
{
m_ColorType.intValue = 0;
}
}
++EditorGUI.indentLevel;
{
@ -415,7 +397,17 @@ namespace UnityEngine.UI
bool showSlicedOrTiled = (!m_Type.hasMultipleDifferentValues && (typeEnum == Image.Type.Sliced || typeEnum == Image.Type.Tiled));
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_ShowSliced.target = (showSlicedOrTiled && !m_Type.hasMultipleDifferentValues && typeEnum == Image.Type.Sliced);
@ -517,15 +509,7 @@ namespace UnityEngine.UI
"None", "Horizontal",
"Vertical", "FourCorner"
};
EditorGUI.BeginChangeCheck();
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);
UXImage.FlipMode flipmodeEnum = (UXImage.FlipMode)m_FlipMode.enumValueIndex;
@ -805,8 +789,12 @@ namespace UnityEngine.UI
GUILayout.Space(EditorGUIUtility.labelWidth);
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");
graphic.SetNativeSize();
EditorUtility.SetDirty(graphic);
@ -819,10 +807,40 @@ namespace UnityEngine.UI
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)
{
int[] ints = (int[])Enum.GetValues(type);
string[] strings = Enum.GetNames(type);
int[] ints = GetEnumValues(type);
string[] strings = GetEnumNames(type);
if (labels.Length != ints.Length)
{
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
guid: 0268df3dd46bb194fa4ae7ec48be7702
guid: 7cc921173c16d4b4ba1fcf1fcaa80479
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -1,7 +1,7 @@
using UnityEditor;
using UnityEditor.DrawUtils;
using UnityEditor.Extensions;
using UnityEditor.SceneManagement;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
@ -11,11 +11,11 @@ namespace UnityEditor.UI
[CanEditMultipleObjects]
internal class UXToggleEditor : UXSelectableEditor
{
SerializedProperty m_OnValueChangedProperty;
SerializedProperty m_TransitionProperty;
SerializedProperty m_GraphicProperty;
SerializedProperty m_GroupProperty;
SerializedProperty m_IsOnProperty;
private SerializedProperty m_OnValueChangedProperty;
private SerializedProperty m_TransitionProperty;
private SerializedProperty m_GraphicProperty;
private SerializedProperty m_GroupProperty;
private SerializedProperty m_IsOnProperty;
private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip;
@ -48,10 +48,8 @@ namespace UnityEditor.UI
private void DrawEventTab()
{
EditorGUILayout.Space();
serializedObject.Update();
EditorGUILayout.PropertyField(m_OnValueChangedProperty);
serializedObject.ApplyModifiedProperties();
}
@ -60,100 +58,88 @@ namespace UnityEditor.UI
GUILayoutHelper.DrawProperty(hoverAudioClip, customSkin, "Hover Sound", "Play", () =>
{
if (hoverAudioClip.objectReferenceValue != null)
{
PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue);
}
});
GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () =>
{
if (clickAudioClip.objectReferenceValue != null)
{
PlayAudio((AudioClip)clickAudioClip.objectReferenceValue);
}
});
}
private void PlayAudio(AudioClip clip)
{
if (clip != null)
{
ExtensionHelper.PreviewAudioClip(clip);
}
}
private void DrawImageTab()
{
EditorGUILayout.Space();
serializedObject.Update();
UXToggle toggle = serializedObject.targetObject as UXToggle;
EditorGUI.BeginChangeCheck();
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);
}
}
}
}
DrawIsOn(toggle);
GUILayoutHelper.DrawProperty(m_TransitionProperty, customSkin, "Transition");
GUILayoutHelper.DrawProperty(m_GraphicProperty, customSkin, "Graphic");
EditorGUI.BeginChangeCheck();
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;
}
DrawGroup(toggle);
EditorGUILayout.Space();
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
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)
{
#if UX_NAVIGATION
#if INPUTSYSTEM_SUPPORT
UXNavigationRuntime.NotifySelection(gameObject);
#endif
if ((flags & ItemInteractionFlags.Select) != 0)
{
host?.HandleSelect(eventData);

View File

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

View File

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

View File

@ -115,10 +115,27 @@ namespace UnityEngine.UI
[Serializable]
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 int _controllerIndex;
[SerializeField] private int _controllerIndexMask = 1;
[SerializeField] private UXBindingProperty _property = UXBindingProperty.GameObjectActive;
[SerializeField] private UXBindingValue _value = new UXBindingValue();
[SerializeField] private List<IndexedValue> _indexedValues = new List<IndexedValue>();
[SerializeField] private UXBindingFallbackMode _fallbackMode = UXBindingFallbackMode.RestoreCapturedDefault;
[SerializeField] private UXBindingValue _fallbackValue = new UXBindingValue();
[HideInInspector] [SerializeField] private UXBindingValue _capturedDefault = new UXBindingValue();
@ -134,7 +151,17 @@ namespace UnityEngine.UI
public int 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
@ -144,6 +171,7 @@ namespace UnityEngine.UI
}
public UXBindingValue Value => _value;
public List<IndexedValue> IndexedValues => _indexedValues;
public UXBindingFallbackMode FallbackMode
{
@ -156,6 +184,13 @@ namespace UnityEngine.UI
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)
{
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;
}
UXBindingPropertyUtility.CaptureValue(target, _property, _capturedDefault);
_capturedProperty = _property;
_hasCapturedDefault = true;
}
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;
}
UXBindingPropertyUtility.CaptureValue(target, _property, _value);
if (selectedIndex == _controllerIndex)
{
_value.CopyFrom(value);
}
}
internal void CaptureCurrentAsFallback(GameObject target)
{
if (target == null || !UXBindingPropertyUtility.IsSupported(target, _property))
if (!UXBindingPropertyUtility.CaptureValue(target, _property, _fallbackValue))
{
return;
}
UXBindingPropertyUtility.CaptureValue(target, _property, _fallbackValue);
}
internal void ResetToCapturedDefault(GameObject target)
@ -215,26 +300,29 @@ namespace UnityEngine.UI
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;
}
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;
}
}
if (selectedIndex == _controllerIndex)
else if (TryGetValue(selectedIndex, out UXBindingValue indexedValue))
{
UXBindingPropertyUtility.ApplyValue(target, _property, _value);
UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, indexedValue);
return;
}
@ -245,14 +333,90 @@ namespace UnityEngine.UI
case UXBindingFallbackMode.RestoreCapturedDefault:
if (_hasCapturedDefault)
{
UXBindingPropertyUtility.ApplyValue(target, _property, _capturedDefault);
UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, _capturedDefault);
}
return;
case UXBindingFallbackMode.UseCustomValue:
UXBindingPropertyUtility.ApplyValue(target, _property, _fallbackValue);
UXBindingPropertyUtility.ApplyValue(in _runtimeTarget, _property, _fallbackValue);
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;
@ -261,7 +425,8 @@ namespace UnityEngine.UI
private bool _initialized;
public UXController Controller => _controller;
public List<BindingEntry> Entries => _entries;
public IReadOnlyList<BindingEntry> Entries => _entries;
internal int RuntimeEntryCount => _entries.Count;
public void Initialize()
{
@ -274,7 +439,34 @@ namespace UnityEngine.UI
NormalizeEntries();
EnsureControllerReference();
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)
@ -291,6 +483,7 @@ namespace UnityEngine.UI
_controller = controller;
RegisterToController();
BuildRuntimeEntries();
}
public void CaptureDefaults()
@ -328,7 +521,20 @@ namespace UnityEngine.UI
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)
@ -338,7 +544,28 @@ namespace UnityEngine.UI
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)
@ -351,18 +578,6 @@ namespace UnityEngine.UI
_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()
{
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()
{
for (int i = 0; i < _entries.Count; i++)

View File

@ -24,6 +24,18 @@ namespace UnityEngine.UI
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
{
private static readonly UXBindingPropertyMetadata[] Metadata =
@ -46,6 +58,12 @@ namespace UnityEngine.UI
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++)
{
if (Metadata[i].Property == property)
@ -57,13 +75,17 @@ namespace UnityEngine.UI
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)
{
return false;
}
resolvedTarget.GameObject = target;
resolvedTarget.Transform = target.transform;
switch (property)
{
case UXBindingProperty.GameObjectActive:
@ -73,22 +95,32 @@ namespace UnityEngine.UI
case UXBindingProperty.CanvasGroupAlpha:
case UXBindingProperty.CanvasGroupInteractable:
case UXBindingProperty.CanvasGroupBlocksRaycasts:
return target.GetComponent<CanvasGroup>() != null;
return target.TryGetComponent(out resolvedTarget.CanvasGroup);
case UXBindingProperty.GraphicColor:
case UXBindingProperty.GraphicMaterial:
return target.GetComponent<Graphic>() != null;
return target.TryGetComponent(out resolvedTarget.Graphic);
case UXBindingProperty.ImageSprite:
return target.GetComponent<Image>() != null;
return target.TryGetComponent(out resolvedTarget.Image);
case UXBindingProperty.TextContent:
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:
return target.GetComponent<RectTransform>() != null;
return target.TryGetComponent(out resolvedTarget.RectTransform);
default:
return false;
}
}
public static bool IsSupported(GameObject target, UXBindingProperty property)
{
return Resolve(target, property, out _);
}
public static void GetSupportedProperties(GameObject target, List<UXBindingProperty> output)
{
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)
{
case UXBindingProperty.GameObjectActive:
destination.BoolValue = target.activeSelf;
return;
destination.BoolValue = target.GameObject.activeSelf;
return true;
case UXBindingProperty.CanvasGroupAlpha:
destination.FloatValue = target.GetComponent<CanvasGroup>().alpha;
return;
if (target.CanvasGroup == null) return false;
destination.FloatValue = target.CanvasGroup.alpha;
return true;
case UXBindingProperty.CanvasGroupInteractable:
destination.BoolValue = target.GetComponent<CanvasGroup>().interactable;
return;
if (target.CanvasGroup == null) return false;
destination.BoolValue = target.CanvasGroup.interactable;
return true;
case UXBindingProperty.CanvasGroupBlocksRaycasts:
destination.BoolValue = target.GetComponent<CanvasGroup>().blocksRaycasts;
return;
if (target.CanvasGroup == null) return false;
destination.BoolValue = target.CanvasGroup.blocksRaycasts;
return true;
case UXBindingProperty.GraphicColor:
destination.ColorValue = target.GetComponent<Graphic>().color;
return;
if (target.Graphic == null) return false;
destination.ColorValue = target.Graphic.color;
return true;
case UXBindingProperty.GraphicMaterial:
destination.ObjectValue = target.GetComponent<Graphic>().material;
return;
if (target.Graphic == null) return false;
destination.ObjectValue = target.Graphic.defaultMaterial;
return true;
case UXBindingProperty.ImageSprite:
destination.ObjectValue = target.GetComponent<Image>().sprite;
return;
if (target.Image == null) return false;
destination.ObjectValue = target.Image.sprite;
return true;
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:
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:
destination.Vector2Value = target.GetComponent<RectTransform>().anchoredPosition;
return;
if (target.RectTransform == null) return false;
destination.Vector2Value = target.RectTransform.anchoredPosition;
return true;
case UXBindingProperty.TransformLocalScale:
destination.Vector3Value = target.transform.localScale;
return;
if (target.Transform == null) return false;
destination.Vector3Value = target.Transform.localScale;
return true;
case UXBindingProperty.TransformLocalEulerAngles:
destination.Vector3Value = target.transform.localEulerAngles;
return;
if (target.Transform == null) return false;
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)
{
case UXBindingProperty.GameObjectActive:
target.SetActive(value.BoolValue);
return;
target.GameObject.SetActive(value.BoolValue);
return true;
case UXBindingProperty.CanvasGroupAlpha:
target.GetComponent<CanvasGroup>().alpha = value.FloatValue;
return;
if (target.CanvasGroup == null) return false;
target.CanvasGroup.alpha = value.FloatValue;
return true;
case UXBindingProperty.CanvasGroupInteractable:
target.GetComponent<CanvasGroup>().interactable = value.BoolValue;
return;
if (target.CanvasGroup == null) return false;
target.CanvasGroup.interactable = value.BoolValue;
return true;
case UXBindingProperty.CanvasGroupBlocksRaycasts:
target.GetComponent<CanvasGroup>().blocksRaycasts = value.BoolValue;
return;
if (target.CanvasGroup == null) return false;
target.CanvasGroup.blocksRaycasts = value.BoolValue;
return true;
case UXBindingProperty.GraphicColor:
target.GetComponent<Graphic>().color = value.ColorValue;
return;
if (target.Graphic == null) return false;
target.Graphic.color = value.ColorValue;
return true;
case UXBindingProperty.GraphicMaterial:
target.GetComponent<Graphic>().material = value.ObjectValue as Material;
return;
if (target.Graphic == null) return false;
target.Graphic.material = value.ObjectValue as Material;
return true;
case UXBindingProperty.ImageSprite:
target.GetComponent<Image>().sprite = value.ObjectValue as Sprite;
return;
if (target.Image == null) return false;
target.Image.sprite = value.ObjectValue as Sprite;
return true;
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:
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:
target.GetComponent<RectTransform>().anchoredPosition = value.Vector2Value;
return;
if (target.RectTransform == null) return false;
target.RectTransform.anchoredPosition = value.Vector2Value;
return true;
case UXBindingProperty.TransformLocalScale:
target.transform.localScale = value.Vector3Value;
return;
if (target.Transform == null) return false;
target.Transform.localScale = value.Vector3Value;
return true;
case UXBindingProperty.TransformLocalEulerAngles:
target.transform.localEulerAngles = value.Vector3Value;
return;
if (target.Transform == null) return false;
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 _name = "Controller";
[SerializeField] private int _length = 2;
[SerializeField] private int _defaultIndex;
[SerializeField] private string _description = string.Empty;
[NonSerialized] private int _selectedIndex = -1;
[NonSerialized] private UXController _owner;
@ -41,6 +42,12 @@ namespace UnityEngine.UI
set => _description = value;
}
public int DefaultIndex
{
get => Mathf.Clamp(_defaultIndex, 0, Length - 1);
set => _defaultIndex = Mathf.Clamp(value, 0, Length - 1);
}
public int 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> _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
{
@ -111,6 +127,18 @@ namespace UnityEngine.UI
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)
{
controller = null;
@ -190,7 +218,11 @@ namespace UnityEngine.UI
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))
{
_bindings.Add(binding);
_runtimeReady = false;
if (Application.isPlaying)
{
RebuildRuntimeEntries();
ApplyCurrentStateToBinding(binding);
}
#if UNITY_EDITOR
if (!Application.isPlaying)
{
@ -226,6 +264,7 @@ namespace UnityEngine.UI
}
_bindings.Remove(binding);
_runtimeReady = false;
#if UNITY_EDITOR
if (!Application.isPlaying)
{
@ -252,10 +291,11 @@ namespace UnityEngine.UI
{
if (_bindings[i] != null)
{
_bindings[i].Initialize();
_bindings[i].RebuildRuntime(this);
}
}
RebuildRuntimeEntries();
ResetAllControllers();
}
@ -278,7 +318,9 @@ namespace UnityEngine.UI
_controllerIdMap.Clear();
_controllerNameMap.Clear();
#if UNITY_EDITOR
var usedNames = new HashSet<string>(StringComparer.Ordinal);
#endif
for (int i = 0; i < _controllers.Count; i++)
{
@ -291,7 +333,9 @@ namespace UnityEngine.UI
controller.EnsureId();
controller.SetOwner(this);
controller.SetSelectedIndexSilently(Mathf.Clamp(controller.SelectedIndex, -1, controller.Length - 1));
controller.DefaultIndex = controller.DefaultIndex;
#if UNITY_EDITOR
if (string.IsNullOrWhiteSpace(controller.Name))
{
controller.Name = $"Controller{i + 1}";
@ -302,10 +346,13 @@ namespace UnityEngine.UI
controller.Name = $"{controller.Name}_{i + 1}";
usedNames.Add(controller.Name);
}
#endif
_controllerIdMap[controller.Id] = i;
_controllerNameMap[controller.Name] = i;
}
_runtimeReady = false;
}
private void CleanupBindings()
@ -333,18 +380,112 @@ namespace UnityEngine.UI
}
controller.SetSelectedIndexSilently(selectedIndex);
NotifyBindings(controller.Id, selectedIndex);
if (TryGetControllerSlot(controller.Id, out int slot))
{
NotifyBindings(slot, selectedIndex);
}
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)
{
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.EventSystems;
namespace UnityEngine.UI
{
@ -12,22 +9,34 @@ namespace UnityEngine.UI
{
[SerializeField] private bool m_AllowSwitchOff = false;
public bool allowSwitchOff
{
get { return m_AllowSwitchOff; }
set { m_AllowSwitchOff = value; }
}
[SerializeField]
private List<UXToggle> m_Toggles = new List<UXToggle>();
private UXToggle[] m_Toggles = new UXToggle[0];
[SerializeField]
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
{
get { return m_DefaultToggle; }
set { m_DefaultToggle = value; EnsureValidState(); }
set
{
m_DefaultToggle = ContainsToggle(value) ? value : null;
EnsureValidState();
}
}
protected UXGroup()
@ -46,157 +55,90 @@ namespace UnityEngine.UI
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)
{
ValidateToggleIsInGroup(toggle);
for (var i = 0; i < m_Toggles.Count; i++)
EnsureStorage();
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;
if (sendCallback)
m_Toggles[i].isOn = false;
item.isOn = false;
else
m_Toggles[i].SetIsOnWithoutNotify(false);
item.SetIsOnWithoutNotify(false);
}
}
public void UnregisterToggle(UXToggle toggle)
{
if (toggle == null)
EnsureStorage();
int index = IndexOfToggle(toggle);
if (index < 0)
return;
if (m_Toggles.Contains(toggle))
m_Toggles.Remove(toggle);
RemoveAt(index);
if (m_DefaultToggle == toggle)
{
m_DefaultToggle = null;
if (m_CurrentToggle == toggle)
{
m_CurrentToggle = null;
m_CurrentIndex = -1;
}
EnsureSingleSelection();
}
public void RegisterToggle(UXToggle toggle)
{
if (toggle == null)
EnsureStorage();
if (toggle == null || ContainsToggle(toggle))
return;
if (!m_Toggles.Contains(toggle))
m_Toggles.Add(toggle);
EnsureCapacity(m_ToggleCount + 1);
m_Toggles[m_ToggleCount] = toggle;
m_ToggleCount++;
if (!allowSwitchOff)
{
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);
}
}
}
}
if (toggle.isOn)
NotifyToggleOn(toggle);
else
EnsureSingleSelection();
}
public bool ContainsToggle(UXToggle toggle)
{
return m_Toggles != null && m_Toggles.Contains(toggle);
EnsureStorage();
return IndexOfToggle(toggle) >= 0;
}
public void EnsureValidState()
{
if (m_Toggles == null)
m_Toggles = new List<UXToggle>();
m_Toggles.RemoveAll(x => x == null);
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;
}
}
EnsureStorage();
CompactNulls();
SyncToggleGroups();
EnsureDefaultToggle();
EnsureSingleSelection();
}
public bool AnyTogglesOn()
{
return m_Toggles.Find(x => x != null && x.isOn) != null;
}
public IEnumerable<UXToggle> ActiveToggles()
{
return m_Toggles.Where(x => x != null && x.isOn);
return FindFirstActiveIndex() >= 0;
}
public UXToggle GetFirstActiveToggle()
{
if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle) && m_DefaultToggle.isOn)
return m_DefaultToggle;
IEnumerable<UXToggle> activeToggles = ActiveToggles();
return activeToggles.Count() > 0 ? activeToggles.First() : null;
int index = FindFirstActiveIndex();
return index >= 0 ? m_Toggles[index] : null;
}
public void SetAllTogglesOff(bool sendCallback = true)
@ -204,19 +146,20 @@ namespace UnityEngine.UI
bool oldAllowSwitchOff = m_AllowSwitchOff;
m_AllowSwitchOff = true;
if (sendCallback)
for (int i = 0; i < m_ToggleCount; i++)
{
for (var i = 0; i < m_Toggles.Count; i++)
if (m_Toggles[i] != null)
m_Toggles[i].isOn = false;
}
else
{
for (var i = 0; i < m_Toggles.Count; i++)
if (m_Toggles[i] != null)
m_Toggles[i].SetIsOnWithoutNotify(false);
UXToggle toggle = m_Toggles[i];
if (toggle == null)
continue;
if (sendCallback)
toggle.isOn = false;
else
toggle.SetIsOnWithoutNotify(false);
}
m_CurrentToggle = null;
m_CurrentIndex = -1;
m_AllowSwitchOff = oldAllowSwitchOff;
}
@ -225,47 +168,229 @@ namespace UnityEngine.UI
SelectAdjacent(true);
}
public void Preview()
public void Previous()
{
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;
UXToggle current = GetFirstActiveToggle();
int currentIndex = current != null ? m_Toggles.IndexOf(current) : -1;
if (!m_AllowSwitchOff)
m_DefaultToggle = m_Toggles[0];
else
m_DefaultToggle = null;
}
int idx = currentIndex;
if (idx == -1 && !forward)
idx = 0;
for (int step = 0; step < m_Toggles.Count; step++)
private void EnsureSingleSelection()
{
int selectedIndex = FindSelectedIndex();
if (selectedIndex < 0 && !m_AllowSwitchOff && m_ToggleCount > 0)
{
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;
}
else
{
idx = (idx - 1 + m_Toggles.Count) % m_Toggles.Count;
UXToggle toggle = m_Toggles[i];
if (toggle != null && i != selectedIndex && toggle.isOn)
toggle.SetIsOnWithoutNotify(false);
}
}
else
{
m_CurrentToggle = null;
m_CurrentIndex = -1;
}
}
UXToggle t = m_Toggles[idx];
if (t == null)
private int FindSelectedIndex()
{
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;
if (!t.IsActive())
continue;
if (!t.IsInteractable())
continue;
t.isOn = true;
toggle.isOn = true;
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
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 42b2d97a2cb439b4395c6dca63357d89, type: 3}
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2
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
{
Pointer = 0,
Gamepad = 1
Keyboard = 1,
Gamepad = 2,
Touch = 3
}
}
#endif

View File

@ -7,10 +7,15 @@ namespace UnityEngine.UI
{
internal sealed class UXInputModeService : MonoBehaviour
{
private const float StickThresholdSqr = 0.04f;
private const float AxisThreshold = 0.2f;
private static UXInputModeService _instance;
private InputAction _pointerAction;
private InputAction _keyboardAction;
private InputAction _gamepadAction;
private InputAction _touchAction;
public static UXInputMode CurrentMode { get; private set; } = UXInputMode.Pointer;
@ -29,7 +34,7 @@ namespace UnityEngine.UI
return _instance;
}
var go = new GameObject("[UXInputModeService]");
GameObject go = new GameObject("[UXInputModeService]");
go.hideFlags = HideFlags.HideAndDontSave;
DontDestroyOnLoad(go);
_instance = go.AddComponent<UXInputModeService>();
@ -47,16 +52,17 @@ namespace UnityEngine.UI
_instance = this;
DontDestroyOnLoad(gameObject);
hideFlags = HideFlags.HideAndDontSave;
CreateActions();
}
private void OnEnable()
{
CreateActions();
SetActionsEnabled(true);
}
private void OnDisable()
{
DisposeActions();
SetActionsEnabled(false);
}
private void OnDestroy()
@ -70,26 +76,63 @@ namespace UnityEngine.UI
private void CreateActions()
{
if (_pointerAction != null || _gamepadAction != null)
if (_pointerAction != null)
{
return;
}
_pointerAction = new InputAction("UXPointerInput", InputActionType.PassThrough);
_pointerAction.AddBinding("<Keyboard>/anyKey");
_pointerAction.AddBinding("<Mouse>/delta");
_pointerAction.AddBinding("<Mouse>/scroll");
_pointerAction.AddBinding("<Mouse>/leftButton");
_pointerAction.AddBinding("<Mouse>/rightButton");
_pointerAction.AddBinding("<Mouse>/middleButton");
_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.AddBinding("<Gamepad>/*");
_gamepadAction.AddBinding("<Joystick>/*");
_gamepadAction.AddBinding("<Gamepad>/buttonSouth");
_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.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()
@ -97,38 +140,62 @@ namespace UnityEngine.UI
if (_pointerAction != null)
{
_pointerAction.performed -= OnPointerInput;
_pointerAction.Disable();
_pointerAction.Dispose();
_pointerAction = null;
}
if (_keyboardAction != null)
{
_keyboardAction.performed -= OnKeyboardInput;
_keyboardAction.Dispose();
_keyboardAction = null;
}
if (_gamepadAction != null)
{
_gamepadAction.performed -= OnGamepadInput;
_gamepadAction.Disable();
_gamepadAction.Dispose();
_gamepadAction = null;
}
if (_touchAction != null)
{
_touchAction.performed -= OnTouchInput;
_touchAction.Dispose();
_touchAction = null;
}
}
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)
{
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)
@ -143,11 +210,11 @@ namespace UnityEngine.UI
case ButtonControl button:
return button.IsPressed();
case StickControl stick:
return stick.ReadValue().sqrMagnitude >= 0.04f;
return stick.ReadValue().sqrMagnitude >= StickThresholdSqr;
case Vector2Control vector2:
return vector2.ReadValue().sqrMagnitude >= 0.04f;
return vector2.ReadValue().sqrMagnitude >= StickThresholdSqr;
case AxisControl axis:
return Mathf.Abs(axis.ReadValue()) >= 0.2f;
return Mathf.Abs(axis.ReadValue()) >= AxisThreshold;
default:
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
using System.Collections.Generic;
using AlicizaX;
using AlicizaX.UI.Runtime;
using UnityEngine.EventSystems;
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 readonly string CacheLayerName = $"Layer{(int)UILayer.All}-{UILayer.All}";
private static IUXNavigationCursorPolicy _cursorPolicy;
private readonly HashSet<UXNavigationScope> _scopeSet = new();
private readonly List<UXNavigationScope> _scopes = new(32);
private readonly HashSet<Transform> _interactiveLayerRoots = new();
private readonly List<UIHolderObjectBase> _holderBuffer = new(32);
private UXNavigationScope[] _scopes = new UXNavigationScope[InitialScopeCapacity];
private int[] _freeIndices = new int[InitialScopeCapacity];
private int _freeCount;
private int _scopeCount;
private int _scopeCapacityHighWater;
private Transform _uiCanvasRoot;
private UXNavigationScope _topScope;
private GameObject _lastObservedSelection;
private bool _discoveryDirty = true;
private ulong _activationSerial;
private bool _missingEventSystemLogged;
private bool _topScopeDirty = true;
private bool _stateDirty = true;
private bool _suppressionDirty = true;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
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();
UXInputModeService.EnsureInstance();
}
@ -41,7 +48,7 @@ namespace UnityEngine.UI
return _instance;
}
var go = new GameObject("[UXNavigationRuntime]");
GameObject go = new GameObject("[UXNavigationRuntime]");
go.hideFlags = HideFlags.HideAndDontSave;
DontDestroyOnLoad(go);
_instance = go.AddComponent<UXNavigationRuntime>();
@ -54,14 +61,33 @@ namespace UnityEngine.UI
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)
{
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()
@ -98,19 +124,42 @@ namespace UnityEngine.UI
internal void RegisterScope(UXNavigationScope scope)
{
if (scope == null || !_scopeSet.Add(scope))
if (scope == null || scope.RuntimeIndex != InvalidIndex)
{
return;
}
_scopes.Add(scope);
_topScopeDirty = true;
_suppressionDirty = true;
int index;
if (_freeCount > 0)
{
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)
{
if (scope == null || !_scopeSet.Remove(scope))
if (scope == null)
{
return;
}
int index = scope.RuntimeIndex;
if (index < 0 || index >= _scopes.Length || _scopes[index] != scope)
{
return;
}
@ -121,42 +170,46 @@ namespace UnityEngine.UI
}
scope.IsAvailable = false;
scope.WasAvailable = false;
scope.SetNavigationSuppressed(false);
scope.RuntimeIndex = InvalidIndex;
_scopes[index] = null;
_freeIndices[_freeCount++] = index;
_scopeCount--;
MarkStateDirty();
}
int idx = _scopes.IndexOf(scope);
if (idx >= 0)
{
int last = _scopes.Count - 1;
_scopes[idx] = _scopes[last];
_scopes.RemoveAt(last);
}
_topScopeDirty = true;
internal void MarkStateDirty()
{
_stateDirty = true;
_suppressionDirty = true;
}
internal void MarkDiscoveryDirty()
internal void MarkSuppressionDirty()
{
_discoveryDirty = true;
_suppressionDirty = true;
}
private void Update()
internal void InvalidateSkipCaches()
{
TryBindUIRoot();
if (_uiCanvasRoot == null)
for (int i = 0; i < _scopeCapacityHighWater; i++)
{
return;
UXNavigationScope scope = _scopes[i];
if (scope != null)
{
scope.InvalidateSkipCacheOnly();
}
}
if (_discoveryDirty)
{
DiscoverScopes();
}
MarkStateDirty();
}
if (_topScopeDirty)
private void FlushStateIfDirty()
{
if (_stateDirty)
{
UXNavigationScope newTopScope = FindTopScope();
_topScopeDirty = false;
_stateDirty = false;
if (!ReferenceEquals(_topScope, newTopScope))
{
_topScope = newTopScope;
@ -170,122 +223,16 @@ namespace UnityEngine.UI
_suppressionDirty = false;
}
if (UXInputModeService.CurrentMode == UXInputMode.Gamepad)
if (UXInputModeService.CurrentMode == UXInputMode.Gamepad || UXInputModeService.CurrentMode == UXInputMode.Keyboard)
{
EnsureGamepadSelection();
if (_topScope != null)
{
Cursor.visible = false;
}
EnsureNavigationSelection();
}
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()
{
UXNavigationScope bestScope = null;
for (int i = 0; i < _scopes.Count; i++)
for (int i = 0; i < _scopeCapacityHighWater; i++)
{
UXNavigationScope scope = _scopes[i];
if (scope == null)
@ -306,12 +253,7 @@ namespace UnityEngine.UI
_suppressionDirty = true;
}
if (!available)
{
continue;
}
if (bestScope == null || CompareScopePriority(scope, bestScope) < 0)
if (available && (bestScope == null || IsHigherPriority(scope, bestScope)))
{
bestScope = scope;
}
@ -320,7 +262,7 @@ namespace UnityEngine.UI
return bestScope;
}
private bool IsScopeAvailable(UXNavigationScope scope)
private static bool IsScopeAvailable(UXNavigationScope scope)
{
if (scope == null || !scope.isActiveAndEnabled || !scope.gameObject.activeInHierarchy)
{
@ -328,64 +270,15 @@ namespace UnityEngine.UI
}
Canvas canvas = scope.Canvas;
if (canvas == null || canvas.gameObject.layer != UIComponent.UIShowLayer)
{
return false;
}
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;
return canvas != null
&& canvas.gameObject.layer == UIComponent.UIShowLayer
&& !scope.IsNavigationSkipped
&& scope.HasAvailableSelectable();
}
private void ApplyScopeSuppression()
{
for (int i = 0; i < _scopes.Count; i++)
for (int i = 0; i < _scopeCapacityHighWater; i++)
{
UXNavigationScope scope = _scopes[i];
if (scope == null)
@ -395,30 +288,17 @@ namespace UnityEngine.UI
bool suppress = scope.IsAvailable
&& _topScope != null
&& !ReferenceEquals(scope, _topScope)
&& scope != _topScope
&& _topScope.BlockLowerScopes
&& CompareScopePriority(_topScope, scope) < 0;
&& IsHigherPriority(_topScope, scope);
scope.SetNavigationSuppressed(suppress);
}
}
private void EnsureGamepadSelection()
private void EnsureNavigationSelection()
{
EventSystem eventSystem = EventSystem.current;
if (eventSystem == null)
{
if (!_missingEventSystemLogged)
{
Debug.LogWarning("UXNavigationRuntime requires an active EventSystem for gamepad navigation.");
_missingEventSystemLogged = true;
}
return;
}
_missingEventSystemLogged = false;
if (_topScope == null || !_topScope.RequireSelectionWhenGamepad)
if (eventSystem == null || _topScope == null || !_topScope.RequireSelectionWhenGamepad)
{
return;
}
@ -432,78 +312,60 @@ namespace UnityEngine.UI
Selectable preferred = _topScope.GetPreferredSelectable();
eventSystem.SetSelectedGameObject(preferred != null ? preferred.gameObject : null);
_lastObservedSelection = eventSystem.currentSelectedGameObject;
if (_lastObservedSelection != null)
GameObject selectedObject = eventSystem.currentSelectedGameObject;
if (selectedObject != null)
{
_topScope.RecordSelection(_lastObservedSelection);
_topScope.RecordSelection(selectedObject);
}
}
private void TrackSelection()
private void RecordSelection(GameObject selectedObject)
{
EventSystem eventSystem = EventSystem.current;
if (eventSystem == null)
if (_stateDirty || _suppressionDirty)
{
_lastObservedSelection = null;
return;
FlushStateIfDirty();
}
GameObject currentSelected = eventSystem.currentSelectedGameObject;
if (ReferenceEquals(_lastObservedSelection, currentSelected))
if (_topScope != null && _topScope.IsSelectableOwnedAndValid(selectedObject))
{
return;
}
_lastObservedSelection = currentSelected;
if (_topScope != null && _topScope.IsSelectableOwnedAndValid(currentSelected))
{
_topScope.RecordSelection(currentSelected);
_topScope.RecordSelection(selectedObject);
}
}
private void OnInputModeChanged(UXInputMode mode)
{
EventSystem eventSystem = EventSystem.current;
if (mode == UXInputMode.Pointer)
_cursorPolicy?.OnInputModeChanged(mode, _topScope);
if (mode == UXInputMode.Gamepad || mode == UXInputMode.Keyboard)
{
if (_topScope != null)
{
Cursor.visible = true;
if (eventSystem != null)
{
eventSystem.SetSelectedGameObject(null);
}
}
_lastObservedSelection = null;
return;
FlushStateIfDirty();
EnsureNavigationSelection();
}
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 rightOrder = right.Canvas != null ? right.Canvas.sortingOrder : int.MinValue;
int orderCompare = rightOrder.CompareTo(leftOrder);
if (orderCompare != 0)
if (leftOrder != rightOrder)
{
return orderCompare;
return leftOrder > rightOrder;
}
int hierarchyCompare = right.GetHierarchyDepth().CompareTo(left.GetHierarchyDepth());
if (hierarchyCompare != 0)
int leftDepth = left.GetHierarchyDepth();
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
using System.Collections.Generic;
using AlicizaX.UI.Runtime;
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
[DisallowMultipleComponent]
[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 List<Selectable> _explicitSelectables = new();
[SerializeField, Header("编辑器烘焙导航控件")] private Selectable[] _bakedSelectables = System.Array.Empty<Selectable>();
[SerializeField, Header("动态控件容量")] private int _runtimeSelectableCapacity = DefaultRuntimeSelectableCapacity;
[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 _autoSelectFirstAvailable = true;
private readonly List<Selectable> _cachedSelectables = new(16);
private readonly HashSet<Selectable> _cachedSelectableSet = new();
private readonly Dictionary<Selectable, Navigation> _baselineNavigation = new();
private readonly List<Selectable> _removeBuffer = new(8);
private readonly List<Selectable> _selectableScanBuffer = new(16);
private Selectable[] _runtimeSelectables;
private Navigation[] _bakedBaselineNavigation;
private Navigation[] _runtimeBaselineNavigation;
private Canvas _cachedCanvas;
private UIHolderObjectBase _cachedHolder;
private Selectable _lastSelected;
private bool _cacheDirty = true;
private bool _navigationSuppressed;
private int _cachedHierarchyDepth = -1;
private bool _cachedIsSkipped;
private bool _isSkippedCacheValid;
private int _runtimeSelectableCount;
internal int RuntimeIndex { get; set; } = InvalidIndex;
internal ulong ActivationSerial { get; set; }
internal bool IsAvailable { 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
{
get => _defaultSelectable;
set => _defaultSelectable = value;
set
{
_defaultSelectable = value;
MarkRuntimeStateDirty();
}
}
public bool RememberLastSelection => _rememberLastSelection;
@ -59,6 +62,7 @@ namespace UnityEngine.UI
_cachedIsSkipped = GetComponentInParent<UXNavigationSkip>(true) != null;
_isSkippedCacheValid = true;
}
return _cachedIsSkipped;
}
}
@ -93,9 +97,16 @@ namespace UnityEngine.UI
}
}
private void Awake()
{
EnsureRuntimeBuffers();
CaptureAllBaselines();
}
private void OnEnable()
{
_cacheDirty = true;
EnsureRuntimeBuffers();
CaptureAllBaselines();
UXNavigationRuntime.EnsureInstance().RegisterScope(this);
}
@ -115,25 +126,108 @@ namespace UnityEngine.UI
private void OnTransformChildrenChanged()
{
_cacheDirty = true;
MarkRuntimeStateDirty();
}
private void OnTransformParentChanged()
{
_cacheDirty = true;
_cachedCanvas = null;
_cachedHolder = null;
_cachedHierarchyDepth = -1;
_isSkippedCacheValid = false;
MarkRuntimeStateDirty();
}
#if UNITY_EDITOR
private void OnValidate()
{
_cacheDirty = true;
if (_runtimeSelectableCapacity < 0)
{
_runtimeSelectableCapacity = 0;
}
}
#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()
{
if (_cachedHierarchyDepth >= 0)
@ -160,14 +254,12 @@ namespace UnityEngine.UI
return false;
}
var nearestScope = target.GetComponentInParent<UXNavigationScope>(true);
UXNavigationScope nearestScope = target.GetComponentInParent<UXNavigationScope>(true);
return nearestScope == this;
}
internal Selectable GetPreferredSelectable()
{
RefreshSelectableCache();
if (_rememberLastSelection && IsSelectableValid(_lastSelected))
{
return _lastSelected;
@ -183,36 +275,20 @@ namespace UnityEngine.UI
return null;
}
for (int i = 0; i < _cachedSelectables.Count; i++)
Selectable selectable = FirstUsable(_bakedSelectables, BakedSelectableCount);
if (selectable != null)
{
Selectable selectable = _cachedSelectables[i];
if (IsSelectableUsable(selectable))
{
return selectable;
}
return selectable;
}
return null;
return FirstUsable(_runtimeSelectables, _runtimeSelectableCount);
}
internal bool HasAvailableSelectable()
{
RefreshSelectableCache();
if (IsSelectableValid(_defaultSelectable))
{
return true;
}
for (int i = 0; i < _cachedSelectables.Count; i++)
{
if (IsSelectableUsable(_cachedSelectables[i]))
{
return true;
}
}
return false;
return IsSelectableValid(_defaultSelectable)
|| FirstUsable(_bakedSelectables, BakedSelectableCount) != null
|| FirstUsable(_runtimeSelectables, _runtimeSelectableCount) != null;
}
internal void RecordSelection(GameObject selectedObject)
@ -241,109 +317,142 @@ namespace UnityEngine.UI
return;
}
RefreshSelectableCache();
CaptureAllBaselines();
_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)
{
continue;
}
if (!_baselineNavigation.ContainsKey(selectable))
{
_baselineNavigation[selectable] = selectable.navigation;
}
if (suppressed)
{
Navigation navigation = selectable.navigation;
navigation.mode = Navigation.Mode.None;
selectable.navigation = navigation;
SetSelectableSuppressed(selectable, true);
}
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;
}
_cacheDirty = false;
_cachedSelectables.Clear();
_cachedSelectableSet.Clear();
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();
Navigation navigation = selectable.navigation;
navigation.mode = Navigation.Mode.None;
selectable.navigation = navigation;
}
public void InvalidateSelectableCache()
private bool ContainsSelectable(Selectable selectable)
{
_cacheDirty = true;
}
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;
}
return IndexOf(_bakedSelectables, BakedSelectableCount, selectable) >= 0
|| IndexOf(_runtimeSelectables, _runtimeSelectableCount, selectable) >= 0;
}
private bool IsSelectableValid(Selectable selectable)
{
return IsSelectableUsable(selectable)
&& (_cachedSelectableSet.Contains(selectable) || Owns(selectable.gameObject));
return IsSelectableUsable(selectable) && ContainsSelectable(selectable);
}
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)
{
return selectable != null
&& selectable.IsActive()
&& selectable.IsInteractable();
return selectable != null && selectable.IsActive() && selectable.IsInteractable();
}
private static Selectable GetSelectableFromObject(GameObject selectedObject)
@ -357,6 +466,21 @@ namespace UnityEngine.UI
? 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

View File

@ -5,6 +5,28 @@ namespace UnityEngine.UI
[AddComponentMenu("UI/UX Navigation Skip")]
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