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 Jovian.InspectorTools { [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 Jovian.InspectorTools.Internal { public class ShowInInspectorAttributeHandler { private const int MAX_DEPTH = 10; private bool _initialized; private readonly UnityObject target; private readonly SerializedObject serializedObject; private List 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 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 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(); 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(); 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 = ""; } 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