using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; namespace UltraCombos { [CustomEditor(typeof(DebugSettingsGUI))] public class DebugSettingsGUIEditor : Editor { // ── Supported member types ──────────────────────────────────────────────── private static readonly HashSet supportedTypes = new() { typeof(bool), typeof(int), typeof(float), typeof(string), typeof(Vector2), typeof(Vector3), typeof(Color) }; private static bool IsSupported(Type t) => supportedTypes.Contains(t) || t.IsEnum; // ── Cached SerializedProperties ─────────────────────────────────────────── private SerializedProperty toggleKeyProp; private SerializedProperty initialRectProp; // Per-entry foldout state (editor-only, not serialized). private readonly List entryFoldouts = new(); // Lazily-built style: foldout triangle + bold text. private static GUIStyle boldFoldout; private static GUIStyle GetBoldFoldout() { if (boldFoldout == null) { boldFoldout = new GUIStyle(EditorStyles.foldout) { fontStyle = FontStyle.Bold }; } return boldFoldout; } private void OnEnable() { toggleKeyProp = serializedObject.FindProperty("toggleKey"); initialRectProp = serializedObject.FindProperty("initialRect"); } // ── Inspector ───────────────────────────────────────────────────────────── public override void OnInspectorGUI() { var gui = (DebugSettingsGUI)target; serializedObject.Update(); EditorGUILayout.LabelField("General", EditorStyles.boldLabel); EditorGUILayout.PropertyField(toggleKeyProp, new GUIContent("Toggle Key")); EditorGUILayout.PropertyField(initialRectProp, new GUIContent("Initial Rect")); serializedObject.ApplyModifiedProperties(); EditorGUILayout.Space(10); EditorGUILayout.LabelField("Entries", EditorStyles.boldLabel); // Sync foldout list length with entries. while (entryFoldouts.Count < gui.entries.Count) { entryFoldouts.Add(true); } for (var i = 0; i < gui.entries.Count; i++) { var (removed, expanded) = DrawEntry(gui, gui.entries[i], i, entryFoldouts[i]); entryFoldouts[i] = expanded; if (removed) { Undo.RecordObject(gui, "Remove Entry"); gui.entries.RemoveAt(i); entryFoldouts.RemoveAt(i); EditorUtility.SetDirty(gui); i--; } EditorGUILayout.Space(2); } EditorGUILayout.Space(4); if (GUILayout.Button("+ Add Entry", GUILayout.Height(26))) { Undo.RecordObject(gui, "Add Entry"); gui.entries.Add(new DebugSettingsGUI.Entry()); entryFoldouts.Add(true); EditorUtility.SetDirty(gui); } } // ── Draw one Entry — returns (removed, expanded) ────────────────────────── private static (bool removed, bool expanded) DrawEntry( DebugSettingsGUI gui, DebugSettingsGUI.Entry entry, int index, bool expanded) { var removed = false; using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { // Header row { var fieldCount = entry.fields.Count; var groupPart = string.IsNullOrEmpty(entry.groupLabel) ? $"Entry {index}" : $"Entry {index} — {entry.groupLabel}"; var summary = fieldCount > 0 ? $"{groupPart} ({fieldCount})" : groupPart; // Reserve one full-width line, then split off a fixed-width button on the right. var lineRect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight); var buttonRect = new Rect(lineRect.xMax - 24, lineRect.y, 22, 18); var foldRect = new Rect(lineRect.x, lineRect.y, lineRect.width - 26, lineRect.height); // Use a style derived from EditorStyles.foldout so the triangle is preserved. expanded = EditorGUI.Foldout(foldRect, expanded, summary, toggleOnLabelClick: true, GetBoldFoldout()); if (GUI.Button(buttonRect, "✕")) { removed = true; } } if (!expanded) { return (removed, expanded); } // Group label EditorGUI.BeginChangeCheck(); var newLabel = EditorGUILayout.TextField("Group Label", entry.groupLabel); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(gui, "Edit Group Label"); entry.groupLabel = newLabel; EditorUtility.SetDirty(gui); } // Target object EditorGUI.BeginChangeCheck(); var newTarget = EditorGUILayout.ObjectField( new GUIContent("Target", "Drag any Component or ScriptableObject here.\n" + "Dropping a GameObject picks its first non-Transform Component."), entry.target, typeof(UnityEngine.Object), allowSceneObjects: true); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(gui, "Set Target"); if (newTarget is GameObject go) { var comps = go.GetComponents(); newTarget = comps.FirstOrDefault(c => c is not Transform) ?? comps.FirstOrDefault() ?? (UnityEngine.Object)go; } entry.target = newTarget; entry.fields.Clear(); EditorUtility.SetDirty(gui); } // Member selection if (entry.target != null) { DrawMemberSelection(gui, entry); } } return (removed, expanded); } // ── Member checkboxes ───────────────────────────────────────────────────── private static void DrawMemberSelection(DebugSettingsGUI gui, DebugSettingsGUI.Entry entry) { var type = entry.target.GetType(); var members = CollectMembers(type); if (members.Count == 0) { EditorGUILayout.HelpBox( "No displayable fields or properties found on this object.\n" + "Supported types: bool · int · float · string · Vector2 · Vector3 · Color · Enum\n" + "Fields must be public or [SerializeField]. Properties must be public with a getter.", MessageType.Info); return; } EditorGUILayout.Space(4); // Select All / None using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("Members to Display", EditorStyles.boldLabel); GUILayout.FlexibleSpace(); if (GUILayout.Button("All", GUILayout.Width(36))) { Undo.RecordObject(gui, "Select All Members"); entry.fields = members.Select(m => m.Name).ToList(); EditorUtility.SetDirty(gui); } if (GUILayout.Button("None", GUILayout.Width(38))) { Undo.RecordObject(gui, "Deselect All Members"); entry.fields.Clear(); EditorUtility.SetDirty(gui); } } // Per-member toggles EditorGUI.indentLevel++; foreach (var mi in members) { var isSelected = entry.fields.Contains(mi.Name); var niceName = ObjectNames.NicifyVariableName(mi.Name); var memberType = GetMemberType(mi); var typeName = FriendlyType(memberType); var hasRange = mi.GetCustomAttribute() != null; var isProperty = mi is PropertyInfo; var suffix = (isProperty ? ", prop" : string.Empty) + (hasRange ? ", Range" : string.Empty); var label = new GUIContent( $"{niceName} ({typeName}{suffix})", tooltip: $"{(isProperty ? "Property" : "Field")}: {mi.Name}\nType: {memberType.FullName}"); EditorGUI.BeginChangeCheck(); var next = EditorGUILayout.ToggleLeft(label, isSelected); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(gui, next ? "Add Member" : "Remove Member"); if (next) { entry.fields.Add(mi.Name); } else { entry.fields.Remove(mi.Name); } EditorUtility.SetDirty(gui); } } EditorGUI.indentLevel--; } // ── Reflection helpers ──────────────────────────────────────────────────── private static List CollectMembers(Type type) { const BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; const BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance; var result = new List(); for (var t = type; t != null && !IsUnityBuiltIn(t); t = t.BaseType) { foreach (var fi in t.GetFields(fieldFlags | BindingFlags.DeclaredOnly)) { // Skip compiler-generated backing fields (e.g. k__BackingField). if (fi.Name[0] == '<') { continue; } var exposed = fi.IsPublic || fi.GetCustomAttribute() != null; if (!exposed) { continue; } if (fi.GetCustomAttribute() != null) { continue; } if (!IsSupported(fi.FieldType)) { continue; } if (!result.Any(r => r.Name == fi.Name)) { result.Add(fi); } } foreach (var pi in t.GetProperties(propFlags | BindingFlags.DeclaredOnly)) { // Must have a getter and no index parameters. if (!pi.CanRead || pi.GetIndexParameters().Length > 0) { continue; } if (pi.GetCustomAttribute() != null) { continue; } if (!IsSupported(pi.PropertyType)) { continue; } if (!result.Any(r => r.Name == pi.Name)) { result.Add(pi); } } } return result; } private static Type GetMemberType(MemberInfo mi) => mi is FieldInfo fi ? fi.FieldType : ((PropertyInfo)mi).PropertyType; private static bool IsUnityBuiltIn(Type t) => t.Namespace != null && t.Namespace.StartsWith("UnityEngine", StringComparison.Ordinal); private static string FriendlyType(Type t) { if (t == typeof(float)) { return "float"; } if (t == typeof(int)) { return "int"; } if (t == typeof(bool)) { return "bool"; } if (t == typeof(string)) { return "string"; } if (t == typeof(Vector2)) { return "Vector2"; } if (t == typeof(Vector3)) { return "Vector3"; } if (t == typeof(Color)) { return "Color"; } if (t.IsEnum) { return t.Name; } return t.Name; } } }