优化UIExtension扩展系统
优化UXBinding编辑器 优化UXBinding性能 优化UXBinding Bug 优化Hotkey注册器性能 优化UXNavigation导航性能
This commit is contained in:
parent
8c3c13634d
commit
526341579a
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 292ca921cd4242d4be9f76bef9bfc08f
|
||||
timeCreated: 1760343702
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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);
|
||||
|
||||
8
Editor/UX/Navigation.meta
Normal file
8
Editor/UX/Navigation.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55d4289f6579aec4eaeeeda649282af4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
202
Editor/UX/Navigation/UXNavigationScopeEditor.cs
Normal file
202
Editor/UX/Navigation/UXNavigationScopeEditor.cs
Normal 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
|
||||
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0268df3dd46bb194fa4ae7ec48be7702
|
||||
guid: 7cc921173c16d4b4ba1fcf1fcaa80479
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9301ee465f2c46d08b2fade637710625
|
||||
timeCreated: 1766136386
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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++)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 42b2d97a2cb439b4395c6dca63357d89, type: 3}
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -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
@ -4,7 +4,9 @@ namespace UnityEngine.UI
|
||||
public enum UXInputMode : byte
|
||||
{
|
||||
Pointer = 0,
|
||||
Gamepad = 1
|
||||
Keyboard = 1,
|
||||
Gamepad = 2,
|
||||
Touch = 3
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user