You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
343 lines
13 KiB
343 lines
13 KiB
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<Type> 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<bool> 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<Component>();
|
|
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<RangeAttribute>() != 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<MemberInfo> CollectMembers(Type type)
|
|
{
|
|
const BindingFlags fieldFlags =
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
|
const BindingFlags propFlags =
|
|
BindingFlags.Public | BindingFlags.Instance;
|
|
|
|
var result = new List<MemberInfo>();
|
|
|
|
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. <Prop>k__BackingField).
|
|
if (fi.Name[0] == '<')
|
|
{
|
|
continue;
|
|
}
|
|
var exposed = fi.IsPublic || fi.GetCustomAttribute<SerializeField>() != null;
|
|
if (!exposed)
|
|
{
|
|
continue;
|
|
}
|
|
if (fi.GetCustomAttribute<HideInInspector>() != 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<HideInInspector>() != 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;
|
|
}
|
|
}
|
|
}
|
|
|