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

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;
}
}
}