2025-12-01 16:44:19 +08:00
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
|
using UnityEditor;
|
|
|
|
|
|
using UnityEditor.Experimental.GraphView;
|
|
|
|
|
|
using UnityEditor.Experimental.SceneManagement;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using AlicizaX.UI;
|
2026-02-26 17:14:47 +08:00
|
|
|
|
using TMPro;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
using UnityEditor.SceneManagement;
|
2026-02-26 17:14:47 +08:00
|
|
|
|
using UnityEngine.UI;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
namespace AlicizaX.UI
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
[CustomEditor(typeof(UXControllerStateRecorder))]
|
|
|
|
|
|
public class UXControllerStateRecorderEditor : UnityEditor.Editor
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private SerializedProperty _stateEntriesProp;
|
|
|
|
|
|
private SerializedProperty _controller;
|
|
|
|
|
|
private SerializedProperty _id;
|
|
|
|
|
|
private UXController _cacheController;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private Dictionary<int, bool> _stateFoldouts = new Dictionary<int, bool>();
|
|
|
|
|
|
private bool _showControllerInfo = true;
|
|
|
|
|
|
private bool _compactMode = false;
|
|
|
|
|
|
|
|
|
|
|
|
private GUIStyle _headerStyle;
|
|
|
|
|
|
private GUIStyle _toolbarStyle;
|
|
|
|
|
|
private GUIStyle _stateHeaderStyle;
|
|
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
_controller = serializedObject.FindProperty("_controller");
|
|
|
|
|
|
_id = serializedObject.FindProperty("_id");
|
|
|
|
|
|
_stateEntriesProp = serializedObject.FindProperty("_stateEntries");
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void OnDestroy()
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (target == null)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
UnRegisterSelf();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void InitStyles()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_headerStyle == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_headerStyle = new GUIStyle(EditorStyles.boldLabel)
|
|
|
|
|
|
{
|
|
|
|
|
|
fontSize = 13,
|
|
|
|
|
|
fontStyle = FontStyle.Bold
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (_toolbarStyle == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_toolbarStyle = new GUIStyle(EditorStyles.toolbar)
|
|
|
|
|
|
{
|
|
|
|
|
|
fixedHeight = 25
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (_stateHeaderStyle == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_stateHeaderStyle = new GUIStyle(EditorStyles.foldout)
|
|
|
|
|
|
{
|
|
|
|
|
|
fontStyle = FontStyle.Bold
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
public override void OnInspectorGUI()
|
|
|
|
|
|
{
|
|
|
|
|
|
InitStyles();
|
|
|
|
|
|
serializedObject.Update();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
var recorder = target as UXControllerStateRecorder;
|
|
|
|
|
|
UXController prefabCtrl = GetPrefabStageController();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
recorder.GenerateID();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
// Header
|
|
|
|
|
|
DrawHeader(recorder);
|
|
|
|
|
|
EditorGUILayout.Space(5);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
// Controller Reference
|
|
|
|
|
|
DrawControllerSection(recorder, prefabCtrl);
|
|
|
|
|
|
EditorGUILayout.Space(5);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
// Toolbar
|
|
|
|
|
|
DrawToolbar();
|
|
|
|
|
|
EditorGUILayout.Space(5);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
// States List
|
|
|
|
|
|
DrawStatesList(recorder);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void DrawHeader(UXControllerStateRecorder recorder)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.LabelField("UX State Recorder", _headerStyle, GUILayout.Width(150));
|
|
|
|
|
|
EditorGUILayout.LabelField($"ID: {_id.intValue}", EditorStyles.miniLabel);
|
|
|
|
|
|
|
|
|
|
|
|
GUILayout.FlexibleSpace();
|
|
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("Refresh", EditorStyles.miniButtonLeft, GUILayout.Width(60)))
|
|
|
|
|
|
{
|
|
|
|
|
|
recorder.Initialize();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Color oldColor = GUI.backgroundColor;
|
|
|
|
|
|
if (_compactMode) GUI.backgroundColor = Color.green;
|
|
|
|
|
|
if (GUILayout.Button("Compact", EditorStyles.miniButtonRight, GUILayout.Width(60)))
|
|
|
|
|
|
{
|
|
|
|
|
|
_compactMode = !_compactMode;
|
|
|
|
|
|
_stateFoldouts.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
GUI.backgroundColor = oldColor;
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
EditorGUILayout.EndVertical();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void DrawControllerSection(UXControllerStateRecorder recorder, UXController prefabCtrl)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (prefabCtrl != null && prefabCtrl != _controller.objectReferenceValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
_controller.objectReferenceValue = prefabCtrl;
|
|
|
|
|
|
recorder.SetController(prefabCtrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (prefabCtrl != null && !prefabCtrl.HasRecorder(recorder))
|
|
|
|
|
|
{
|
|
|
|
|
|
RegisterSelfToController(prefabCtrl);
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (_controller.objectReferenceValue != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_cacheController = _controller.objectReferenceValue as UXController;
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
|
_showControllerInfo = EditorGUILayout.Foldout(_showControllerInfo, "Controller Reference", true);
|
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
|
|
|
|
|
|
// Controller dropdown
|
|
|
|
|
|
var availableControllers = GetAvailableControllers(recorder);
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
|
EditorGUILayout.LabelField("Controller", GUILayout.Width(70));
|
|
|
|
|
|
|
|
|
|
|
|
if (availableControllers.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
int currentIndex = -1;
|
|
|
|
|
|
var currentController = _controller.objectReferenceValue as UXController;
|
|
|
|
|
|
if (currentController != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
currentIndex = availableControllers.IndexOf(currentController);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string[] options = new string[availableControllers.Count];
|
|
|
|
|
|
for (int i = 0; i < availableControllers.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ctrl = availableControllers[i];
|
|
|
|
|
|
string path = GetGameObjectPath(ctrl.gameObject, recorder.gameObject);
|
|
|
|
|
|
options[i] = $"{ctrl.gameObject.name} ({path})";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
|
|
int newIndex = EditorGUILayout.Popup(currentIndex, options);
|
|
|
|
|
|
if (EditorGUI.EndChangeCheck() && newIndex >= 0 && newIndex < availableControllers.Count)
|
|
|
|
|
|
{
|
|
|
|
|
|
_controller.objectReferenceValue = availableControllers[newIndex];
|
|
|
|
|
|
recorder.SetController(availableControllers[newIndex]);
|
|
|
|
|
|
if (!availableControllers[newIndex].HasRecorder(recorder))
|
|
|
|
|
|
{
|
|
|
|
|
|
RegisterSelfToController(availableControllers[newIndex]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (currentController != null && GUILayout.Button("Select", GUILayout.Width(60)))
|
|
|
|
|
|
{
|
|
|
|
|
|
Selection.activeGameObject = currentController.gameObject;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUILayout.LabelField("(No controllers found in parents)", EditorStyles.miniLabel);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
// Controller info
|
|
|
|
|
|
if (_showControllerInfo && _controller.objectReferenceValue != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
UXController ctl = _controller.objectReferenceValue as UXController;
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUI.indentLevel++;
|
|
|
|
|
|
EditorGUILayout.LabelField($"Controllers: {ctl.ControllerCount}", EditorStyles.miniLabel);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
for (int i = 0; i < ctl.ControllerCount; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var handle = ctl.GetControllerAt(i);
|
|
|
|
|
|
if (handle != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
string desc = string.IsNullOrEmpty(handle.Description) ? "" : $" - {handle.Description}";
|
|
|
|
|
|
EditorGUILayout.LabelField($" [{i}] {handle.Name} (Length: {handle.Length}){desc}", EditorStyles.miniLabel);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
EditorGUI.indentLevel--;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_controller.objectReferenceValue == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUILayout.HelpBox("No controller assigned. Add UXController to parent GameObject.", MessageType.Warning);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void DrawToolbar()
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.BeginHorizontal(_toolbarStyle);
|
|
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("+ Add State", EditorStyles.toolbarButton, GUILayout.Width(100)))
|
|
|
|
|
|
{
|
|
|
|
|
|
ShowAddStateMenu();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUI.BeginDisabledGroup(_stateEntriesProp.arraySize == 0);
|
|
|
|
|
|
if (GUILayout.Button("Clear All", EditorStyles.toolbarButton, GUILayout.Width(70)))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (EditorUtility.DisplayDialog("Clear All States",
|
|
|
|
|
|
"Remove all states?", "Clear", "Cancel"))
|
|
|
|
|
|
{
|
|
|
|
|
|
ClearAllStates();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("Expand All", EditorStyles.toolbarButton, GUILayout.Width(70)))
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < _stateEntriesProp.arraySize; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
_stateFoldouts[i] = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("Collapse All", EditorStyles.toolbarButton, GUILayout.Width(75)))
|
|
|
|
|
|
{
|
|
|
|
|
|
_stateFoldouts.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
GUILayout.FlexibleSpace();
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.LabelField($"States: {_stateEntriesProp.arraySize}", EditorStyles.miniLabel, GUILayout.Width(70));
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void DrawStatesList(UXControllerStateRecorder recorder)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_stateEntriesProp.arraySize == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUILayout.HelpBox("No states defined. Click '+ Add State' to begin.", MessageType.Info);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < _stateEntriesProp.arraySize; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
DrawStateEntry(recorder, i);
|
|
|
|
|
|
EditorGUILayout.Space(3);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void DrawStateEntry(UXControllerStateRecorder recorder, int index)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (index < 0 || index >= _stateEntriesProp.arraySize)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var entryProp = _stateEntriesProp.GetArrayElementAtIndex(index);
|
|
|
|
|
|
if (entryProp == null) return;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
|
|
|
|
|
var stateProp = entryProp.FindPropertyRelative("State");
|
|
|
|
|
|
var controllerNameProp = entryProp.FindPropertyRelative("ControllerName");
|
|
|
|
|
|
var controllerIndexProp = entryProp.FindPropertyRelative("ControllerIndex");
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
// Validate all properties
|
|
|
|
|
|
if (controllerNameProp == null || controllerIndexProp == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (stateProp == null || stateProp.managedReferenceValue == null)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
DrawInvalidStateEntry(index);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var stateInstance = stateProp.managedReferenceValue as ControllerStateBase;
|
|
|
|
|
|
var stateType = stateProp.managedReferenceValue.GetType();
|
|
|
|
|
|
var attr = stateType.GetCustomAttribute<ControlerStateNameAttribute>();
|
|
|
|
|
|
var stateName = attr != null ? attr.StateName : stateType.Name;
|
|
|
|
|
|
|
|
|
|
|
|
bool isValid = ValidateState(recorder, stateInstance);
|
|
|
|
|
|
bool isFolded = _stateFoldouts.ContainsKey(index) && _stateFoldouts[index];
|
|
|
|
|
|
|
|
|
|
|
|
// State box with validation color
|
|
|
|
|
|
Color boxColor = GUI.backgroundColor;
|
|
|
|
|
|
if (!isValid) GUI.backgroundColor = new Color(1f, 0.5f, 0.5f);
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
|
|
|
|
GUI.backgroundColor = boxColor;
|
|
|
|
|
|
|
|
|
|
|
|
// Header
|
|
|
|
|
|
DrawStateHeader(index, stateName, ref isFolded);
|
|
|
|
|
|
_stateFoldouts[index] = isFolded;
|
|
|
|
|
|
|
|
|
|
|
|
// Compact mode: show binding info
|
|
|
|
|
|
if (_compactMode && !isFolded)
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUI.indentLevel++;
|
|
|
|
|
|
try
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
string bindingText = string.IsNullOrEmpty(controllerNameProp.stringValue)
|
|
|
|
|
|
? "Not Bound"
|
|
|
|
|
|
: $"Bound to: {controllerNameProp.stringValue}[{controllerIndexProp.intValue}]";
|
|
|
|
|
|
EditorGUILayout.LabelField(bindingText, EditorStyles.miniLabel);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
2026-02-26 17:14:47 +08:00
|
|
|
|
catch (ObjectDisposedException)
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUILayout.LabelField("Binding: (refreshing...)", EditorStyles.miniLabel);
|
|
|
|
|
|
}
|
|
|
|
|
|
EditorGUI.indentLevel--;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Expanded content
|
|
|
|
|
|
if (isFolded && !_compactMode)
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUI.indentLevel++;
|
|
|
|
|
|
|
|
|
|
|
|
// Binding
|
|
|
|
|
|
DrawStateBinding(recorder, controllerNameProp, controllerIndexProp);
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.Space(3);
|
|
|
|
|
|
|
|
|
|
|
|
// Properties
|
|
|
|
|
|
if (stateInstance != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
|
EditorGUILayout.LabelField("Properties", EditorStyles.boldLabel);
|
|
|
|
|
|
|
|
|
|
|
|
// Set to Current Value button
|
|
|
|
|
|
if (GUILayout.Button("Set to Current", GUILayout.Width(110)))
|
|
|
|
|
|
{
|
|
|
|
|
|
SetStateToCurrentValue(recorder, stateInstance, stateProp);
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
|
|
|
|
|
|
SerializedClassDrawer.DrawSerializableProperty(stateProp);
|
|
|
|
|
|
|
|
|
|
|
|
// Description
|
|
|
|
|
|
EditorGUILayout.Space(2);
|
|
|
|
|
|
var description = stateInstance.GetDescription();
|
|
|
|
|
|
if (!string.IsNullOrEmpty(description))
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUILayout.LabelField($"ℹ {description}", EditorStyles.miniLabel);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validation warning
|
|
|
|
|
|
if (!isValid)
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUILayout.HelpBox("State validation failed. Check required components.", MessageType.Error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUI.indentLevel--;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void DrawStateHeader(int index, string stateName, ref bool isFolded)
|
|
|
|
|
|
{
|
2025-12-01 16:44:19 +08:00
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
// Foldout with name
|
|
|
|
|
|
isFolded = EditorGUILayout.Foldout(isFolded, $"[{index}] {stateName}", true, _stateHeaderStyle);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
GUILayout.FlexibleSpace();
|
|
|
|
|
|
|
|
|
|
|
|
// Buttons
|
|
|
|
|
|
if (GUILayout.Button("▲", EditorStyles.miniButtonLeft, GUILayout.Width(25)) && index > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
MoveStateUp(index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("▼", EditorStyles.miniButtonMid, GUILayout.Width(25)) && index < _stateEntriesProp.arraySize - 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
MoveStateDown(index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("⎘", EditorStyles.miniButtonMid, GUILayout.Width(25)))
|
|
|
|
|
|
{
|
|
|
|
|
|
DuplicateState(index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("×", EditorStyles.miniButtonRight, GUILayout.Width(25)))
|
|
|
|
|
|
{
|
|
|
|
|
|
DeleteStateAtIndex(index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void DrawStateBinding(UXControllerStateRecorder recorder,
|
|
|
|
|
|
SerializedProperty controllerNameProp, SerializedProperty controllerIndexProp)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Safety check: ensure properties are valid
|
|
|
|
|
|
if (controllerNameProp == null || controllerIndexProp == null)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.HelpBox("Binding properties are invalid. Try refreshing.", MessageType.Warning);
|
|
|
|
|
|
return;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
|
|
|
|
EditorGUILayout.LabelField("Binding", EditorStyles.boldLabel);
|
|
|
|
|
|
|
|
|
|
|
|
var controllerNames = GetControllerNamesForRecorder(recorder);
|
|
|
|
|
|
|
|
|
|
|
|
// Controller dropdown
|
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
|
EditorGUILayout.LabelField("Controller", GUILayout.Width(70));
|
|
|
|
|
|
|
2025-12-01 16:44:19 +08:00
|
|
|
|
if (controllerNames.Count > 0)
|
|
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
string currentName = "";
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
currentName = controllerNameProp.stringValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (ObjectDisposedException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Property has been disposed, exit gracefully
|
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int selIdx = controllerNames.IndexOf(currentName);
|
|
|
|
|
|
if (selIdx < 0) selIdx = 0;
|
|
|
|
|
|
|
|
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
|
|
int newSel = EditorGUILayout.Popup(selIdx, controllerNames.ToArray());
|
|
|
|
|
|
if (EditorGUI.EndChangeCheck() && newSel >= 0 && newSel < controllerNames.Count)
|
|
|
|
|
|
{
|
2025-12-01 16:44:19 +08:00
|
|
|
|
controllerNameProp.stringValue = controllerNames[newSel];
|
2026-02-26 17:14:47 +08:00
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.LabelField("(No controllers available)", EditorStyles.miniLabel);
|
|
|
|
|
|
}
|
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
|
|
|
|
|
|
// Index dropdown
|
|
|
|
|
|
if (controllerNames.Count > 0 && !string.IsNullOrEmpty(controllerNameProp.stringValue))
|
|
|
|
|
|
{
|
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
|
EditorGUILayout.LabelField("Index", GUILayout.Width(70));
|
|
|
|
|
|
|
|
|
|
|
|
int ctrlLen = GetSelectedControllerLength(recorder, controllerNameProp.stringValue);
|
|
|
|
|
|
if (ctrlLen <= 0) ctrlLen = 1;
|
|
|
|
|
|
|
|
|
|
|
|
controllerIndexProp.intValue = Mathf.Clamp(controllerIndexProp.intValue, 0, ctrlLen - 1);
|
|
|
|
|
|
|
|
|
|
|
|
string[] idxOptions = new string[ctrlLen];
|
|
|
|
|
|
for (int j = 0; j < ctrlLen; j++) idxOptions[j] = j.ToString();
|
|
|
|
|
|
|
|
|
|
|
|
int curIdx = controllerIndexProp.intValue;
|
|
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
|
|
int newIdx = EditorGUILayout.Popup(curIdx, idxOptions, GUILayout.Width(100));
|
|
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
controllerIndexProp.intValue = newIdx;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
2026-02-26 17:14:47 +08:00
|
|
|
|
|
|
|
|
|
|
// Preview button
|
|
|
|
|
|
if (_controller.objectReferenceValue != null && !Application.isPlaying)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (GUILayout.Button("Preview", GUILayout.Width(60)))
|
|
|
|
|
|
{
|
|
|
|
|
|
PreviewState(controllerNameProp.stringValue, controllerIndexProp.intValue);
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
2026-02-26 17:14:47 +08:00
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void DrawInvalidStateEntry(int index)
|
|
|
|
|
|
{
|
|
|
|
|
|
Color oldColor = GUI.backgroundColor;
|
|
|
|
|
|
GUI.backgroundColor = new Color(1f, 0.5f, 0.5f);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
|
|
|
|
GUI.backgroundColor = oldColor;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
|
EditorGUILayout.LabelField($"[{index}] Invalid State", EditorStyles.boldLabel);
|
|
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("Remove", GUILayout.Width(60)))
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
DeleteStateAtIndex(index);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.EndHorizontal();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.HelpBox("State data is corrupted. Remove and re-add.", MessageType.Error);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void DeleteStateAtIndex(int index)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (index < 0 || index >= _stateEntriesProp.arraySize)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
Debug.LogWarning($"Invalid state index: {index}");
|
|
|
|
|
|
return;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
Undo.RecordObject(serializedObject.targetObject, "Delete State");
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
// Get the entry before deleting
|
|
|
|
|
|
var entryProp = _stateEntriesProp.GetArrayElementAtIndex(index);
|
|
|
|
|
|
var stateProp = entryProp?.FindPropertyRelative("State");
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
// If the state reference is not null, we need to delete twice
|
|
|
|
|
|
// First deletion clears the reference, second removes the array element
|
|
|
|
|
|
if (stateProp != null && stateProp.managedReferenceValue != null)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
stateProp.managedReferenceValue = null;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
2026-02-26 17:14:47 +08:00
|
|
|
|
|
|
|
|
|
|
// Now delete the array element
|
|
|
|
|
|
_stateEntriesProp.DeleteArrayElementAtIndex(index);
|
|
|
|
|
|
|
|
|
|
|
|
// Clean up foldout dictionary
|
|
|
|
|
|
_stateFoldouts.Remove(index);
|
|
|
|
|
|
|
|
|
|
|
|
// Rebuild foldout dictionary with corrected indices
|
|
|
|
|
|
var newFoldouts = new Dictionary<int, bool>();
|
|
|
|
|
|
foreach (var kvp in _stateFoldouts)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (kvp.Key < index)
|
|
|
|
|
|
{
|
|
|
|
|
|
newFoldouts[kvp.Key] = kvp.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (kvp.Key > index)
|
|
|
|
|
|
{
|
|
|
|
|
|
newFoldouts[kvp.Key - 1] = kvp.Value;
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
2026-02-26 17:14:47 +08:00
|
|
|
|
_stateFoldouts = newFoldouts;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
EditorUtility.SetDirty(serializedObject.targetObject);
|
|
|
|
|
|
|
|
|
|
|
|
// Exit GUI to prevent using disposed SerializedProperty
|
|
|
|
|
|
GUIUtility.ExitGUI();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ClearAllStates()
|
|
|
|
|
|
{
|
|
|
|
|
|
Undo.RecordObject(serializedObject.targetObject, "Clear All States");
|
|
|
|
|
|
|
|
|
|
|
|
// Clear all managed references first
|
|
|
|
|
|
for (int i = _stateEntriesProp.arraySize - 1; i >= 0; i--)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
var entryProp = _stateEntriesProp.GetArrayElementAtIndex(i);
|
|
|
|
|
|
var stateProp = entryProp?.FindPropertyRelative("State");
|
|
|
|
|
|
if (stateProp != null && stateProp.managedReferenceValue != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
stateProp.managedReferenceValue = null;
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
2026-02-26 17:14:47 +08:00
|
|
|
|
|
|
|
|
|
|
// Clear the array
|
|
|
|
|
|
_stateEntriesProp.ClearArray();
|
|
|
|
|
|
_stateFoldouts.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
EditorUtility.SetDirty(serializedObject.targetObject);
|
|
|
|
|
|
|
|
|
|
|
|
// Exit GUI to prevent using disposed SerializedProperty
|
|
|
|
|
|
GUIUtility.ExitGUI();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void PreviewState(string controllerName, int index)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_controller.objectReferenceValue != null)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
var ctl = _controller.objectReferenceValue as UXController;
|
|
|
|
|
|
ctl.SetControllerIndex(controllerName, index);
|
|
|
|
|
|
EditorUtility.SetDirty(ctl);
|
|
|
|
|
|
SceneView.RepaintAll();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
2026-02-26 17:14:47 +08:00
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void DuplicateState(int index)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (index < 0 || index >= _stateEntriesProp.arraySize)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
Debug.LogWarning($"Invalid state index: {index}");
|
|
|
|
|
|
return;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
Undo.RecordObject(serializedObject.targetObject, "Duplicate State");
|
|
|
|
|
|
|
|
|
|
|
|
_stateEntriesProp.InsertArrayElementAtIndex(index);
|
|
|
|
|
|
|
|
|
|
|
|
// Update foldout dictionary
|
|
|
|
|
|
var newFoldouts = new Dictionary<int, bool>();
|
|
|
|
|
|
foreach (var kvp in _stateFoldouts)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (kvp.Key <= index)
|
|
|
|
|
|
{
|
|
|
|
|
|
newFoldouts[kvp.Key] = kvp.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
newFoldouts[kvp.Key + 1] = kvp.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_stateFoldouts = newFoldouts;
|
|
|
|
|
|
_stateFoldouts[index + 1] = true;
|
|
|
|
|
|
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
EditorUtility.SetDirty(serializedObject.targetObject);
|
|
|
|
|
|
|
|
|
|
|
|
// Exit GUI to prevent using disposed SerializedProperty
|
|
|
|
|
|
GUIUtility.ExitGUI();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void MoveStateUp(int index)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (index <= 0 || index >= _stateEntriesProp.arraySize)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
Undo.RecordObject(serializedObject.targetObject, "Move State Up");
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
_stateEntriesProp.MoveArrayElement(index, index - 1);
|
|
|
|
|
|
|
|
|
|
|
|
// Update foldout dictionary
|
|
|
|
|
|
if (_stateFoldouts.ContainsKey(index))
|
|
|
|
|
|
{
|
|
|
|
|
|
bool wasFolded = _stateFoldouts[index];
|
|
|
|
|
|
_stateFoldouts.Remove(index);
|
|
|
|
|
|
_stateFoldouts[index - 1] = wasFolded;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
EditorUtility.SetDirty(serializedObject.targetObject);
|
|
|
|
|
|
|
|
|
|
|
|
// Exit GUI to prevent using disposed SerializedProperty
|
|
|
|
|
|
GUIUtility.ExitGUI();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void MoveStateDown(int index)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (index < 0 || index >= _stateEntriesProp.arraySize - 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Undo.RecordObject(serializedObject.targetObject, "Move State Down");
|
|
|
|
|
|
|
|
|
|
|
|
_stateEntriesProp.MoveArrayElement(index, index + 1);
|
|
|
|
|
|
|
|
|
|
|
|
// Update foldout dictionary
|
|
|
|
|
|
if (_stateFoldouts.ContainsKey(index))
|
|
|
|
|
|
{
|
|
|
|
|
|
bool wasFolded = _stateFoldouts[index];
|
|
|
|
|
|
_stateFoldouts.Remove(index);
|
|
|
|
|
|
_stateFoldouts[index + 1] = wasFolded;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
EditorUtility.SetDirty(serializedObject.targetObject);
|
|
|
|
|
|
|
|
|
|
|
|
// Exit GUI to prevent using disposed SerializedProperty
|
|
|
|
|
|
GUIUtility.ExitGUI();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
2026-02-26 17:14:47 +08:00
|
|
|
|
|
|
|
|
|
|
private bool ValidateState(UXControllerStateRecorder recorder, ControllerStateBase stateInstance)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (stateInstance == null) return false;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
return stateInstance.Valid(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void ShowAddStateMenu()
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
var provider = ScriptableObject.CreateInstance<StateTypeSearchProvider>();
|
|
|
|
|
|
provider.Init(OnTypeSelected);
|
|
|
|
|
|
SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(Event.current.mousePosition)), provider);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void OnTypeSelected(Type type)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (type == null) return;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
Undo.RecordObject(serializedObject.targetObject, "Add Controller State");
|
|
|
|
|
|
serializedObject.Update();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
int idx = _stateEntriesProp.arraySize;
|
|
|
|
|
|
_stateEntriesProp.InsertArrayElementAtIndex(idx);
|
|
|
|
|
|
var entryProp = _stateEntriesProp.GetArrayElementAtIndex(idx);
|
|
|
|
|
|
var stateProp = entryProp.FindPropertyRelative("State");
|
|
|
|
|
|
var controllerNameProp = entryProp.FindPropertyRelative("ControllerName");
|
|
|
|
|
|
var controllerIndexProp = entryProp.FindPropertyRelative("ControllerIndex");
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
ControllerStateBase instance = null;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
instance = (ControllerStateBase)Activator.CreateInstance(type);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogException(ex);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (instance != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var recorder = serializedObject.targetObject as UXControllerStateRecorder;
|
|
|
|
|
|
instance.Init(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (stateProp != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
stateProp.managedReferenceValue = instance;
|
|
|
|
|
|
stateProp.isExpanded = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
controllerNameProp.stringValue = "";
|
|
|
|
|
|
controllerIndexProp.intValue = 0;
|
|
|
|
|
|
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
EditorUtility.SetDirty(serializedObject.targetObject);
|
|
|
|
|
|
|
|
|
|
|
|
_stateFoldouts[idx] = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UnRegisterSelf()
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
UXController controller = (_cacheController != null) ? _cacheController : GetPrefabStageController();
|
|
|
|
|
|
if (controller == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
var so = new SerializedObject(controller);
|
|
|
|
|
|
var recorders = so.FindProperty("_recorders");
|
|
|
|
|
|
for (int i = recorders.arraySize - 1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
var el = recorders.GetArrayElementAtIndex(i);
|
|
|
|
|
|
if (el.objectReferenceValue == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
recorders.DeleteArrayElementAtIndex(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
so.ApplyModifiedProperties();
|
|
|
|
|
|
EditorUtility.SetDirty(controller);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void RegisterSelfToController(UXController controller)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
var so = new SerializedObject(controller);
|
|
|
|
|
|
var recorders = so.FindProperty("_recorders");
|
|
|
|
|
|
int newIndex = recorders.arraySize;
|
|
|
|
|
|
recorders.InsertArrayElementAtIndex(newIndex);
|
|
|
|
|
|
var el = recorders.GetArrayElementAtIndex(newIndex);
|
|
|
|
|
|
el.objectReferenceValue = target;
|
|
|
|
|
|
so.ApplyModifiedProperties();
|
|
|
|
|
|
EditorUtility.SetDirty(controller);
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private UXController GetPrefabStageController()
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
var stage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
|
|
|
|
if (stage == null) return null;
|
|
|
|
|
|
var root = stage.prefabContentsRoot;
|
|
|
|
|
|
if (root == null) return null;
|
|
|
|
|
|
return root.GetComponentInChildren<UXController>();
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private List<string> GetControllerNamesForRecorder(UXControllerStateRecorder recorder)
|
|
|
|
|
|
{
|
|
|
|
|
|
var names = new List<string>();
|
|
|
|
|
|
var prefabCtrl = GetPrefabStageController();
|
|
|
|
|
|
if (prefabCtrl != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var cd in prefabCtrl.Controllers) names.Add(cd.Name);
|
|
|
|
|
|
return names;
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
var assigned = recorder.Controller;
|
|
|
|
|
|
if (assigned != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var cd in assigned.Controllers) names.Add(cd.Name);
|
|
|
|
|
|
return names;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var inScene = recorder.gameObject.GetComponentInParent<UXController>();
|
|
|
|
|
|
if (inScene != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var cd in inScene.Controllers) names.Add(cd.Name);
|
|
|
|
|
|
return names;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return names;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private int GetSelectedControllerLength(UXControllerStateRecorder recorder, string controllerName)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
var prefabCtrl = GetPrefabStageController();
|
|
|
|
|
|
if (prefabCtrl != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ch = prefabCtrl.Controllers.ToList().Find(x => x.Name == controllerName);
|
|
|
|
|
|
if (ch != null) return ch.Length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var assigned = recorder.Controller;
|
|
|
|
|
|
if (assigned != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ch = assigned.Controllers.ToList().Find(x => x.Name == controllerName);
|
|
|
|
|
|
if (ch != null) return ch.Length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var inScene = recorder.gameObject.GetComponentInParent<UXController>();
|
|
|
|
|
|
if (inScene != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ch = inScene.Controllers.ToList().Find(x => x.Name == controllerName);
|
|
|
|
|
|
if (ch != null) return ch.Length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return 1;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private List<UXController> GetAvailableControllers(UXControllerStateRecorder recorder)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
var controllers = new List<UXController>();
|
|
|
|
|
|
|
|
|
|
|
|
// Search upwards through all parent transforms
|
|
|
|
|
|
Transform current = recorder.transform.parent;
|
|
|
|
|
|
while (current != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ctrl = current.GetComponent<UXController>();
|
|
|
|
|
|
if (ctrl != null && !controllers.Contains(ctrl))
|
|
|
|
|
|
{
|
|
|
|
|
|
controllers.Add(ctrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
current = current.parent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return controllers;
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private string GetGameObjectPath(GameObject target, GameObject from)
|
2025-12-01 16:44:19 +08:00
|
|
|
|
{
|
2026-02-26 17:14:47 +08:00
|
|
|
|
if (target == null || from == null) return "";
|
|
|
|
|
|
|
|
|
|
|
|
List<string> path = new List<string>();
|
|
|
|
|
|
Transform current = from.transform.parent;
|
|
|
|
|
|
|
|
|
|
|
|
while (current != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (current.gameObject == target)
|
|
|
|
|
|
{
|
|
|
|
|
|
return path.Count == 0 ? "Parent" : string.Join("/", path);
|
|
|
|
|
|
}
|
|
|
|
|
|
path.Insert(0, "..");
|
|
|
|
|
|
current = current.parent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return "Unknown";
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 17:14:47 +08:00
|
|
|
|
private void SetStateToCurrentValue(UXControllerStateRecorder recorder, ControllerStateBase state, SerializedProperty stateProp)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (recorder == null || state == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
Undo.RecordObject(recorder, "Set State to Current Value");
|
|
|
|
|
|
|
|
|
|
|
|
// Use reflection to set current values based on state type
|
|
|
|
|
|
var stateType = state.GetType();
|
|
|
|
|
|
|
|
|
|
|
|
if (state is TransformPositionState)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (recorder.TryGetComponent<RectTransform>(out var rect))
|
|
|
|
|
|
{
|
|
|
|
|
|
var posField = stateType.GetField("_position", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (posField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
posField.SetValue(state, rect.anchoredPosition);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is TransformScaleState)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (recorder.transform != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var scaleField = stateType.GetField("_scale", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (scaleField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
scaleField.SetValue(state, recorder.transform.localScale);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is TransformRotationState)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (recorder.transform != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var rotField = stateType.GetField("_rotation", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (rotField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
rotField.SetValue(state, recorder.transform.localEulerAngles);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is GraphicColorState)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (recorder.TryGetComponent<Graphic>(out var graphic))
|
|
|
|
|
|
{
|
|
|
|
|
|
var colorField = stateType.GetField("_color", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (colorField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
colorField.SetValue(state, graphic.color);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is GraphicMaterialState)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (recorder.TryGetComponent<Graphic>(out var graphic))
|
|
|
|
|
|
{
|
|
|
|
|
|
var matField = stateType.GetField("_material", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (matField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
matField.SetValue(state, graphic.material);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is ImageSpriteState)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (recorder.TryGetComponent<Image>(out var image))
|
|
|
|
|
|
{
|
|
|
|
|
|
var spriteField = stateType.GetField("_sprite", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (spriteField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
spriteField.SetValue(state, image.sprite);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is TextContentState)
|
|
|
|
|
|
{
|
|
|
|
|
|
string currentText = "";
|
|
|
|
|
|
if (recorder.TryGetComponent<Text>(out var text))
|
|
|
|
|
|
{
|
|
|
|
|
|
currentText = text.text;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (recorder.TryGetComponent<TextMeshProUGUI>(out var tmp))
|
|
|
|
|
|
{
|
|
|
|
|
|
currentText = tmp.text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var textField = stateType.GetField("_text", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (textField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
textField.SetValue(state, currentText);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is TextColorState)
|
|
|
|
|
|
{
|
|
|
|
|
|
Color currentColor = Color.white;
|
|
|
|
|
|
if (recorder.TryGetComponent<Text>(out var text))
|
|
|
|
|
|
{
|
|
|
|
|
|
currentColor = text.color;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (recorder.TryGetComponent<TextMeshProUGUI>(out var tmp))
|
|
|
|
|
|
{
|
|
|
|
|
|
currentColor = tmp.color;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var colorField = stateType.GetField("_color", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (colorField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
colorField.SetValue(state, currentColor);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is CanvasGroupAlphaState)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (recorder.TryGetComponent<CanvasGroup>(out var canvasGroup))
|
|
|
|
|
|
{
|
|
|
|
|
|
var alphaField = stateType.GetField("_alpha", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (alphaField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
alphaField.SetValue(state, canvasGroup.alpha);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is CanvasGroupInteractableState)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (recorder.TryGetComponent<CanvasGroup>(out var canvasGroup))
|
|
|
|
|
|
{
|
|
|
|
|
|
var interField = stateType.GetField("_interactable", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
var blockField = stateType.GetField("_blocksRaycasts", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (interField != null && blockField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
interField.SetValue(state, canvasGroup.interactable);
|
|
|
|
|
|
blockField.SetValue(state, canvasGroup.blocksRaycasts);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (state is GameObjectActiveState)
|
|
|
|
|
|
{
|
|
|
|
|
|
var activeField = stateType.GetField("_active", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
|
|
if (activeField != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
activeField.SetValue(state, recorder.gameObject.activeSelf);
|
|
|
|
|
|
serializedObject.ApplyModifiedProperties();
|
|
|
|
|
|
serializedObject.Update();
|
|
|
|
|
|
EditorUtility.SetDirty(recorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Debug.Log($"Set {stateType.Name} to current value");
|
|
|
|
|
|
}
|
2025-12-01 16:44:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|