Files
trail-into-darkness/Packages/com.jovian.inspector/Editor/Attributes/ShowInInspectorAttribute.cs
2026-03-29 18:59:24 +02:00

375 lines
15 KiB
C#

using System;
using System.Linq;
using UnityEngine;
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using Jovian.Utilities;
using UnityObject = UnityEngine.Object;
#endif
namespace InspectorToolkit {
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class ShowInInspectorAttribute : PropertyAttribute {
public readonly PlayModeVisibility Visibility;
public readonly bool isEdittingSupported;
public ShowInInspectorAttribute() {
Visibility = PlayModeVisibility.PlayMode;
isEdittingSupported = false;
}
public ShowInInspectorAttribute(PlayModeVisibility playModeVisibility, bool isEdittingSupported = false) {
Visibility = playModeVisibility;
this.isEdittingSupported = isEdittingSupported;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
public class ShowInInspectorAttributeHandler {
private const int MAX_DEPTH = 10;
private bool _initialized;
private readonly UnityObject target;
private readonly SerializedObject serializedObject;
private List<Member> showMembers;
private GUIStyle labelStyle;
private GUIStyle tooltipStyle;
private class Member {
public MemberInfo member;
public readonly PlayModeVisibility visibility;
public readonly bool isEditingSupported;
private readonly object parent;
public bool supportsExpansion;
public bool expanded;
public Member parentMember;
public int depth;
public string niceName;
public string name;
public bool IsChild => parentMember != null;
public bool IsParentExpanded => parentMember.expanded;
public Member(MemberInfo member,
PlayModeVisibility visibility,
bool isEditingSupported,
object parent,
int depth,
Member parentMember) {
this.member = member;
this.visibility = visibility;
this.isEditingSupported = isEditingSupported;
this.parent = parent;
this.parentMember = parentMember;
this.depth = depth;
name = member.Name;
niceName = ObjectNames.NicifyVariableName(name);
}
public bool TryGetValueAndData(out object value) {
switch (member) {
case FieldInfo fieldInfo:
value = fieldInfo.GetValue(parent);
return true;
case PropertyInfo propertyInfo:
value = propertyInfo.GetValue(parent);
return true;
default:
value = null;
return false;
}
}
public Type GetValueType() {
return member switch {
FieldInfo fieldInfo => fieldInfo.FieldType,
PropertyInfo propertyInfo => propertyInfo.PropertyType,
_ => throw new NotSupportedException()
};
}
public void SetValue(object value) {
switch (member) {
case FieldInfo fieldInfo:
fieldInfo.SetValue(parent, value);
break;
case PropertyInfo propertyInfo:
propertyInfo.SetValue(parent, value);
break;
}
}
}
public bool HasValues => showMembers.Count > 0;
public ShowInInspectorAttributeHandler(UnityObject target, SerializedObject serializedObject) {
this.target = target;
this.serializedObject = serializedObject;
showMembers = new();
}
public void Update() {
serializedObject.Update();
Setup();
}
private void Setup() {
if (_initialized) {
return;
}
showMembers.Clear();
int fieldCount = EditorTypes.GetFields(target, out List<FieldInfo> objectFields);
for (int i = 0; i < fieldCount; i++) {
var showAttribute = Attribute.GetCustomAttribute(objectFields[i], typeof(ShowInInspectorAttribute)) as ShowInInspectorAttribute;
if (showAttribute == null) {
continue;
}
SetupMember(objectFields[i], objectFields[i].GetValue(target), showAttribute.Visibility, showAttribute.isEdittingSupported, target);
}
int propertiesCount = EditorTypes.GetProperties(target, out List<PropertyInfo> objectProperties);
for (int i = 0; i < propertiesCount; i++) {
var showAttribute = Attribute.GetCustomAttribute(objectProperties[i], typeof(ShowInInspectorAttribute)) as ShowInInspectorAttribute;
if (showAttribute == null) {
continue;
}
SetupMember(objectProperties[i], objectProperties[i].GetValue(target), showAttribute.Visibility, showAttribute.isEdittingSupported, target);
}
_initialized = true;
}
private void SetupMember(MemberInfo memberInfo,
object memberValue,
PlayModeVisibility visibility,
bool isEditingSupported,
object parent,
int depth = 0,
Member parentMember = null) {
Member member = new(memberInfo, visibility, isEditingSupported, parent, depth, parentMember);
if (depth >= MAX_DEPTH) {
return;
}
showMembers.Add(member);
if (memberValue != null && CanValueExpand(memberValue)) {
Type memberType = memberValue.GetType();
foreach (FieldInfo fieldInfo in memberType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) {
ShowInInspectorAttribute attribute = fieldInfo.GetCustomAttribute<ShowInInspectorAttribute>();
if (attribute != null) {
SetupMember(fieldInfo, fieldInfo.GetValue(memberValue), attribute.Visibility, attribute.isEdittingSupported, memberValue, depth + 1, member);
member.supportsExpansion = true;
}
}
foreach (PropertyInfo propertyInfo in memberType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) {
ShowInInspectorAttribute attribute = propertyInfo.GetCustomAttribute<ShowInInspectorAttribute>();
if (attribute != null) {
SetupMember(propertyInfo, propertyInfo.GetValue(memberValue), attribute.Visibility, attribute.isEdittingSupported, memberValue, depth + 1, member);
member.supportsExpansion = true;
}
}
}
}
private bool TryGetValueTypeFromEnumerable(object value, out Type type) {
Type[] arguments = value.GetType().GenericTypeArguments;
if (arguments.Length > 0) {
if (value is IDictionary) {
type = value.GetType().GenericTypeArguments[1]; // value
return true;
}
else if (value is IEnumerable) {
type = value.GetType().GenericTypeArguments[0];
return true;
}
}
type = null;
return false;
}
public void OnInspectorGUI(Editor editor) {
labelStyle ??= new GUIStyle(EditorStyles.label) {
alignment = TextAnchor.UpperLeft,
padding = new RectOffset(0, 0, 2, 2)
};
tooltipStyle ??= new GUIStyle(EditorStyles.miniTextField) {
fixedHeight = 0f,
stretchHeight = true,
wordWrap = true,
stretchWidth = false,
imagePosition = ImagePosition.TextOnly,
clipping = TextClipping.Overflow,
border = new RectOffset(0, 0, 0, 0)
};
int indentLevel = EditorGUI.indentLevel;
foreach (Member entry in showMembers) {
if (entry.visibility.IsVisible() == false || (entry.IsChild && entry.IsParentExpanded == false)) {
continue;
}
Type valueType = entry.GetValueType();
string stringValue = "";
object value = null;
EditorGUI.indentLevel = indentLevel + entry.depth;
if (entry.TryGetValueAndData(out value)) {
if (value is IList list) {
stringValue = list.ListToString(true);
}
else if (value is IEnumerable enumerable) {
stringValue = enumerable.EnumerableToString(true);
}
else {
stringValue = value?.ToString();
}
}
else {
stringValue = "<NULL>";
}
string labelName = entry.niceName;
if (entry.supportsExpansion) {
entry.expanded = EditorGUILayout.Foldout(entry.expanded, labelName, true);
}
else {
bool isGUIEnabled = GUI.enabled;
bool allowEdit = entry.isEditingSupported;
GUI.enabled = allowEdit;
Rect entryRect;
if (value is IEnumerable enumerable && TryGetValueTypeFromEnumerable(value, out Type enumValueType)) {
string labelText = $"{labelName}";
if (value is ICollection collection) {
labelText += $" (Count={collection.Count})";
}
string collectionInfo = $"({value.GetType().Name}<{enumValueType.Name}>)";
GUILayout.BeginHorizontal();
entry.expanded = EditorGUILayout.Foldout(entry.expanded, labelText, false);
GUILayout.FlexibleSpace();
GUILayout.Label(collectionInfo, EditorStyles.miniLabel);
GUILayout.EndHorizontal();
entryRect = GUILayoutUtility.GetLastRect();
if (entry.expanded) {
EditorGUI.indentLevel++;
int counter = 0;
if (enumerable is IDictionary dictionary) {
foreach (DictionaryEntry keyValuePair in dictionary) {
object newValue = InspectorGUIUtility.DrawField(keyValuePair.Key.ToString(), enumValueType, keyValuePair.Value);
if (newValue != value && allowEdit) {
entry.SetValue(newValue);
}
}
}
else {
foreach (var item in enumerable) {
object newValue = InspectorGUIUtility.DrawField((counter++).ToString(), enumValueType, item);
if (newValue != value && allowEdit) {
entry.SetValue(newValue);
}
}
}
EditorGUI.indentLevel--;
}
}
else {
object newValue = InspectorGUIUtility.DrawField(labelName, valueType, value);
entryRect = GUILayoutUtility.GetLastRect();
if (newValue != value && allowEdit) {
entry.SetValue(newValue);
}
}
GUI.enabled = isGUIEnabled;
if (entryRect.Contains(Event.current.mousePosition)) {
Vector2 tooltipPosition = Event.current.mousePosition;
float width = entryRect.width - (tooltipPosition.x - entryRect.x);
tooltipStyle.fixedWidth = width;
var tooltipContent = new GUIContent(stringValue, stringValue);
float height = tooltipStyle.CalcHeight(tooltipContent, width);
Vector2 tooltipSize = new(width, height);
tooltipPosition.y -= tooltipSize.y;
var tooltipRect = new Rect(tooltipPosition, tooltipSize);
GUI.Label(tooltipRect, tooltipContent, tooltipStyle);
if (Event.current.type == EventType.ContextClick) {
GenericMenu menu = new();
menu.AddItem(new GUIContent("Copy"), false, () => { EditorGUIUtility.systemCopyBuffer = stringValue; });
menu.ShowAsContext();
Event.current.Use();
}
editor.Repaint();
}
}
}
EditorGUI.indentLevel = indentLevel;
}
private static readonly Type[] NonExpandedTypes = new Type[] {
typeof(string),
typeof(decimal),
typeof(DateTime),
typeof(DateTimeOffset),
typeof(TimeSpan),
typeof(Guid)
};
// Use this to stop expansion of types we dont want, including Unity Objects - they just link to the object.
private static bool CanValueExpand(object value) {
if (value == null) {
return false;
}
Type type = value.GetType();
if (IsSimpleType(type)) {
return false;
}
if (type == typeof(UnityEngine.Object) || type.IsSubclassOf(typeof(UnityEngine.Object))) {
return false;
}
return true;
}
//https://stackoverflow.com/a/32337906/584774
private static bool IsSimpleType(Type type) {
return type.IsPrimitive || type.IsEnum || NonExpandedTypes.Contains(type) || Convert.GetTypeCode(type) != TypeCode.Object ||
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0]));
}
}
}
#endif