forked from Shardstone/trail-into-darkness
375 lines
15 KiB
C#
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
|