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.
 
 
 

308 lines
12 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace UltraCombos
{
[CustomEditor(typeof(DebugMonitorHUD))]
public class DebugMonitorHUDEditor : 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 displayTextProp;
private SerializedProperty refreshRateProp;
private void OnEnable()
{
displayTextProp = serializedObject.FindProperty("displayText");
refreshRateProp = serializedObject.FindProperty("refreshRate");
}
// ── Inspector ─────────────────────────────────────────────────────────────
public override void OnInspectorGUI()
{
var hud = (DebugMonitorHUD)target;
serializedObject.Update();
EditorGUILayout.LabelField("Display", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(displayTextProp, new GUIContent("Display Text",
"The TextMeshProUGUI component that will show the monitored values."));
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("General", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(refreshRateProp, new GUIContent("Refresh Rate"));
serializedObject.ApplyModifiedProperties();
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Entries", EditorStyles.boldLabel);
for (var i = 0; i < hud.entries.Count; i++)
{
var removed = DrawEntry(hud, hud.entries[i], i);
if (removed)
{
Undo.RecordObject(hud, "Remove Entry");
hud.entries.RemoveAt(i);
EditorUtility.SetDirty(hud);
i--;
}
EditorGUILayout.Space(2);
}
EditorGUILayout.Space(4);
if (GUILayout.Button("+ Add Entry", GUILayout.Height(26)))
{
Undo.RecordObject(hud, "Add Entry");
hud.entries.Add(new DebugMonitorHUD.Entry());
EditorUtility.SetDirty(hud);
}
}
// ── Draw one Entry — returns true when the user clicked Remove ─────────────
private static bool DrawEntry(DebugMonitorHUD hud, DebugMonitorHUD.Entry entry, int index)
{
var removed = false;
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
// Header row
using (new EditorGUILayout.HorizontalScope())
{
var title = string.IsNullOrEmpty(entry.groupLabel)
? $"Entry {index}"
: $"Entry {index} — {entry.groupLabel}";
EditorGUILayout.LabelField(title, EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
if (GUILayout.Button("✕", GUILayout.Width(22), GUILayout.Height(18)))
{
removed = true;
}
}
// Group label
EditorGUI.BeginChangeCheck();
var newLabel = EditorGUILayout.TextField("Group Label", entry.groupLabel);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(hud, "Edit Group Label");
entry.groupLabel = newLabel;
EditorUtility.SetDirty(hud);
}
// 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(hud, "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(hud);
}
// Member selection
if (entry.target != null)
{
DrawMemberSelection(hud, entry);
}
}
return removed;
}
// ── Member checkboxes ─────────────────────────────────────────────────────
private static void DrawMemberSelection(DebugMonitorHUD hud, DebugMonitorHUD.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(hud, "Select All Members");
entry.fields = members.Select(m => m.Name).ToList();
EditorUtility.SetDirty(hud);
}
if (GUILayout.Button("None", GUILayout.Width(38)))
{
Undo.RecordObject(hud, "Deselect All Members");
entry.fields.Clear();
EditorUtility.SetDirty(hud);
}
}
// 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(hud, next ? "Add Member" : "Remove Member");
if (next)
{
entry.fields.Add(mi.Name);
}
else
{
entry.fields.Remove(mi.Name);
}
EditorUtility.SetDirty(hud);
}
}
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;
}
}
}