#if UNITY_EDITOR using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace AlicizaX.UI { [CustomEditor(typeof(UXBinding))] public sealed class UXBindingEditor : UnityEditor.Editor { private SerializedProperty _controllerProp; private SerializedProperty _entriesProp; private readonly Dictionary _foldouts = new Dictionary(); private readonly List _supportedProperties = new List(); private void OnEnable() { _controllerProp = serializedObject.FindProperty("_controller"); _entriesProp = serializedObject.FindProperty("_entries"); } public override void OnInspectorGUI() { serializedObject.Update(); var binding = (UXBinding)target; DrawHeader(binding); EditorGUILayout.Space(6f); DrawControllerField(binding); EditorGUILayout.Space(6f); DrawEntries(binding); serializedObject.ApplyModifiedProperties(); } 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); EditorGUI.BeginChangeCheck(); UXController newController = (UXController)EditorGUILayout.ObjectField("Reference", binding.Controller, typeof(UXController), true); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(binding, "Change UX Binding Controller"); binding.SetController(newController); _controllerProp.objectReferenceValue = newController; EditorUtility.SetDirty(binding); } 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.EndVertical(); } private void DrawEntries(UXBinding binding) { if (_entriesProp.arraySize == 0) { EditorGUILayout.HelpBox("No rules yet. Add one and choose a controller + property pair.", MessageType.Info); return; } UXBindingPropertyUtility.GetSupportedProperties(binding.gameObject, _supportedProperties); for (int i = 0; i < _entriesProp.arraySize; i++) { DrawEntry(binding, i); EditorGUILayout.Space(4f); } } private void DrawEntry(UXBinding binding, int index) { SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index); SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId"); SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex"); SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property"); SerializedProperty valueProp = entryProp.FindPropertyRelative("_value"); 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; EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); expanded = EditorGUILayout.Foldout(expanded, $"[{index}] {label}", true); GUILayout.FlexibleSpace(); if (GUILayout.Button("Preview", EditorStyles.miniButtonLeft, GUILayout.Width(60f))) { binding.PreviewEntry(index); SceneView.RepaintAll(); } if (GUILayout.Button("X", EditorStyles.miniButtonRight, GUILayout.Width(24f))) { DeleteEntry(binding, index); } EditorGUILayout.EndHorizontal(); _foldouts[index] = expanded; if (expanded) { DrawControllerSelector(binding, controllerIdProp, controllerIndexProp); DrawPropertySelector(entryProp, binding.gameObject, propertyProp); property = (UXBindingProperty)propertyProp.enumValueIndex; UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(property); EditorGUILayout.Space(2f); EditorGUILayout.LabelField("Value", EditorStyles.boldLabel); DrawValueField(valueProp, metadata, "Matched Value"); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Use Current")) { 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")) { binding.CaptureEntryFallbackValue(index); EditorUtility.SetDirty(binding); } } if (!_supportedProperties.Contains(property)) { EditorGUILayout.HelpBox("This property is not supported by the components on the current GameObject.", MessageType.Error); } } EditorGUILayout.EndVertical(); } private void DrawControllerSelector(UXBinding binding, SerializedProperty controllerIdProp, SerializedProperty controllerIndexProp) { UXController controller = binding.Controller; if (controller == null || controller.Controllers.Count == 0) { EditorGUILayout.HelpBox("Create a controller definition first.", MessageType.Info); return; } string[] names = new string[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; if (definition.Id == controllerIdProp.stringValue) { selectedController = i; } } EditorGUI.BeginChangeCheck(); selectedController = EditorGUILayout.Popup("Controller", selectedController, names); if (EditorGUI.EndChangeCheck()) { controllerIdProp.stringValue = controller.Controllers[selectedController].Id; controllerIndexProp.intValue = 0; } UXController.ControllerDefinition selectedDefinition = controller.Controllers[selectedController]; int maxIndex = Mathf.Max(1, selectedDefinition.Length); controllerIndexProp.intValue = Mathf.Clamp(controllerIndexProp.intValue, 0, maxIndex - 1); 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(_supportedProperties); UXBindingProperty current = (UXBindingProperty)propertyProp.enumValueIndex; if (!options.Contains(current)) { options.Add(current); } string[] displayNames = new string[options.Count]; int selectedIndex = 0; for (int i = 0; i < options.Count; i++) { UXBindingProperty option = options[i]; UXBindingPropertyMetadata metadata = UXBindingPropertyUtility.GetMetadata(option); bool supported = UXBindingPropertyUtility.IsSupported(targetObject, option); displayNames[i] = supported ? metadata.DisplayName : $"{metadata.DisplayName} (Unsupported)"; if (option == current) { selectedIndex = i; } } EditorGUI.BeginChangeCheck(); selectedIndex = EditorGUILayout.Popup("Property", selectedIndex, displayNames); if (EditorGUI.EndChangeCheck()) { propertyProp.enumValueIndex = (int)options[selectedIndex]; entryProp.FindPropertyRelative("_hasCapturedDefault").boolValue = false; entryProp.FindPropertyRelative("_capturedProperty").enumValueIndex = propertyProp.enumValueIndex; ResetValue(entryProp.FindPropertyRelative("_capturedDefault")); ApplyDefaultFallbackForProperty(entryProp, (UXBindingProperty)propertyProp.enumValueIndex); } } private void DrawValueField(SerializedProperty valueProp, UXBindingPropertyMetadata metadata, string label) { switch (metadata.ValueKind) { case UXBindingValueKind.Boolean: { SerializedProperty boolProp = valueProp.FindPropertyRelative("_boolValue"); boolProp.boolValue = EditorGUILayout.Toggle(label, boolProp.boolValue); break; } case UXBindingValueKind.Float: { SerializedProperty floatProp = valueProp.FindPropertyRelative("_floatValue"); floatProp.floatValue = EditorGUILayout.FloatField(label, floatProp.floatValue); break; } case UXBindingValueKind.String: { SerializedProperty stringProp = valueProp.FindPropertyRelative("_stringValue"); EditorGUILayout.LabelField(label); stringProp.stringValue = EditorGUILayout.TextArea(stringProp.stringValue, GUILayout.MinHeight(54f)); break; } case UXBindingValueKind.Color: { SerializedProperty colorProp = valueProp.FindPropertyRelative("_colorValue"); colorProp.colorValue = EditorGUILayout.ColorField(label, colorProp.colorValue); break; } case UXBindingValueKind.Vector2: { SerializedProperty vector2Prop = valueProp.FindPropertyRelative("_vector2Value"); vector2Prop.vector2Value = EditorGUILayout.Vector2Field(label, vector2Prop.vector2Value); break; } case UXBindingValueKind.Vector3: { SerializedProperty vector3Prop = valueProp.FindPropertyRelative("_vector3Value"); vector3Prop.vector3Value = EditorGUILayout.Vector3Field(label, vector3Prop.vector3Value); break; } case UXBindingValueKind.ObjectReference: { SerializedProperty objectProp = valueProp.FindPropertyRelative("_objectValue"); objectProp.objectReferenceValue = EditorGUILayout.ObjectField( label, objectProp.objectReferenceValue, metadata.ObjectReferenceType, false); break; } } } private void AddEntry(UXBinding binding) { int index = _entriesProp.arraySize; _entriesProp.InsertArrayElementAtIndex(index); SerializedProperty entryProp = _entriesProp.GetArrayElementAtIndex(index); SerializedProperty controllerIdProp = entryProp.FindPropertyRelative("_controllerId"); SerializedProperty controllerIndexProp = entryProp.FindPropertyRelative("_controllerIndex"); SerializedProperty propertyProp = entryProp.FindPropertyRelative("_property"); SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode"); SerializedProperty valueProp = entryProp.FindPropertyRelative("_value"); SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue"); SerializedProperty capturedDefaultProp = entryProp.FindPropertyRelative("_capturedDefault"); SerializedProperty hasCapturedDefaultProp = entryProp.FindPropertyRelative("_hasCapturedDefault"); SerializedProperty capturedPropertyProp = entryProp.FindPropertyRelative("_capturedProperty"); controllerIdProp.stringValue = string.Empty; controllerIndexProp.intValue = 0; propertyProp.enumValueIndex = (int)UXBindingProperty.GameObjectActive; fallbackModeProp.enumValueIndex = (int)UXBindingFallbackMode.RestoreCapturedDefault; ResetValue(valueProp); 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; } _foldouts[index] = true; } private static void ResetValue(SerializedProperty valueProp) { valueProp.FindPropertyRelative("_boolValue").boolValue = false; valueProp.FindPropertyRelative("_floatValue").floatValue = 0f; valueProp.FindPropertyRelative("_stringValue").stringValue = string.Empty; valueProp.FindPropertyRelative("_colorValue").colorValue = Color.white; valueProp.FindPropertyRelative("_vector2Value").vector2Value = Vector2.zero; valueProp.FindPropertyRelative("_vector3Value").vector3Value = Vector3.zero; valueProp.FindPropertyRelative("_objectValue").objectReferenceValue = null; } private void DeleteEntry(UXBinding binding, int index) { if (index < 0 || index >= _entriesProp.arraySize) { return; } Undo.RecordObject(binding, "Delete UX Binding Rule"); _entriesProp.DeleteArrayElementAtIndex(index); CleanupFoldouts(index); serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(binding); GUIUtility.ExitGUI(); } private void CleanupFoldouts(int removedIndex) { _foldouts.Remove(removedIndex); var remapped = new Dictionary(); foreach (var pair in _foldouts) { int nextIndex = pair.Key > removedIndex ? pair.Key - 1 : pair.Key; remapped[nextIndex] = pair.Value; } _foldouts.Clear(); foreach (var pair in remapped) { _foldouts[pair.Key] = pair.Value; } } private static void ApplyDefaultFallbackForProperty(SerializedProperty entryProp, UXBindingProperty property) { SerializedProperty fallbackModeProp = entryProp.FindPropertyRelative("_fallbackMode"); SerializedProperty fallbackValueProp = entryProp.FindPropertyRelative("_fallbackValue"); if (property == UXBindingProperty.GameObjectActive) { fallbackModeProp.enumValueIndex = (int)UXBindingFallbackMode.UseCustomValue; fallbackValueProp.FindPropertyRelative("_boolValue").boolValue = false; return; } if ((UXBindingFallbackMode)fallbackModeProp.enumValueIndex == UXBindingFallbackMode.UseCustomValue) { fallbackModeProp.enumValueIndex = (int)UXBindingFallbackMode.RestoreCapturedDefault; ResetValue(fallbackValueProp); } } } } #endif