using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; namespace UDE_HAND_INTERACTION { public class EditorBase { private SerializedObject _serializedObject; private HashSet _hiddenProperties = new HashSet(); private HashSet _skipProperties = new HashSet(); private Dictionary> _customDrawers = new Dictionary>(); private Dictionary _sections = new Dictionary(); private List _orderedSections = new List(); public class Section { public string title; public bool isFoldout; public bool foldout; public List properties = new List(); public bool HasSomethingToDraw() { return properties != null && properties.Count > 0; } } public EditorBase(SerializedObject serializedObject) { _serializedObject = serializedObject; } #region Sections private Section GetOrCreateSection(string sectionName) { if (_sections.TryGetValue(sectionName, out Section existingSection)) { return existingSection; } Section section = CreateSection(sectionName, false); return section; } public Section CreateSection(string sectionName, bool isFoldout) { if (_sections.TryGetValue(sectionName, out Section existingSection)) { Debug.LogError($"Section {sectionName} already exists"); return null; } Section section = new Section() { title = sectionName, isFoldout = isFoldout }; _sections.Add(sectionName, section); _orderedSections.Add(sectionName); return section; } public void CreateSections(Dictionary sections, bool isFoldout) { foreach (var sectionData in sections) { CreateSection(sectionData.Key, isFoldout); AddToSection(sectionData.Key, sectionData.Value); } } public void AddToSection(string sectionName, params string[] properties) { if (properties.Length == 0 || !ValidateProperties(properties)) { return; } Section section = GetOrCreateSection(sectionName); foreach (var property in properties) { section.properties.Add(property); _skipProperties.Add(property); } } #endregion #region IMPLEMENTATION public bool IsInSection(string property) { return _skipProperties.Contains(property); } public bool IsHidden(string property) { return _hiddenProperties.Contains(property); } public void DrawFullInspector() { SerializedProperty it = _serializedObject.GetIterator(); it.NextVisible(enterChildren: true); EditorGUI.BeginDisabledGroup(true); EditorGUILayout.PropertyField(it); EditorGUI.EndDisabledGroup(); EditorGUI.BeginChangeCheck(); while (it.NextVisible(enterChildren: false)) { if (IsInSection(it.name)) { continue; } DrawProperty(it); } foreach (string sectionKey in _orderedSections) { DrawSection(sectionKey); } _serializedObject.ApplyModifiedProperties(); } public void DrawSection(string sectionName) { Section section = _sections[sectionName]; DrawSection(section); } private void DrawSection(Section section) { if (!section.HasSomethingToDraw()) { return; } if (section.isFoldout) { section.foldout = EditorGUILayout.Foldout(section.foldout, section.title); if (!section.foldout) { return; } EditorGUI.indentLevel++; } foreach (string prop in section.properties) { DrawProperty(_serializedObject.FindProperty(prop)); } if (section.isFoldout) { EditorGUI.indentLevel--; } } private void DrawProperty(SerializedProperty property) { try { if (IsHidden(property.name)) { return; } Action customDrawer; if (_customDrawers.TryGetValue(property.name, out customDrawer)) { customDrawer(property); } else { EditorGUILayout.PropertyField(property, includeChildren: true); } } catch (Exception e) { Debug.LogError($"Error drawing property {e.Message}"); } } private bool ValidateProperties(params string[] properties) { foreach (var property in properties) { if (_serializedObject.FindProperty(property) == null) { Debug.LogWarning( $"Could not find property {property}, maybe it was deleted or renamed?"); return false; } } return true; } #endregion } public class OptionalAttribute : PropertyAttribute { [System.Flags] public enum Flag { None = 0, AutoGenerated = 1 << 0, DontHide = 1 << 1 } public Flag Flags { get; private set; } = Flag.None; public OptionalAttribute() { } public OptionalAttribute(Flag flags) { Flags = flags; } } public class SectionAttribute : PropertyAttribute { public string SectionName { get; private set; } = string.Empty; public SectionAttribute(string sectionName) { SectionName = sectionName; } } public class EditorUtils : Editor { protected EditorBase _editorDrawer; private const string OptionalSection = "Optionals"; protected virtual void OnEnable() { _editorDrawer = new EditorBase(serializedObject); _editorDrawer.CreateSections(FindCustomSections(serializedObject), true); _editorDrawer.CreateSection(OptionalSection, true); _editorDrawer.AddToSection(OptionalSection, FindOptionals(serializedObject)); } public override void OnInspectorGUI() { _editorDrawer.DrawFullInspector(); } private static string[] FindOptionals(SerializedObject serializedObject) { List> props = new List>(); UnityEngine.Object obj = serializedObject.targetObject; if (obj != null) { FindAttributedSerializedFields(obj.GetType(), props); } return props.Where(p => (p.attribute.Flags & OptionalAttribute.Flag.DontHide) == 0) .Select(p => p.propertyName) .ToArray(); } private static Dictionary FindCustomSections(SerializedObject serializedObject) { List> props = new List>(); UnityEngine.Object obj = serializedObject.targetObject; if (obj != null) { FindAttributedSerializedFields(obj.GetType(), props); } Dictionary sections = new Dictionary(); var namedSections = props.GroupBy(p => p.attribute.SectionName); foreach (var namedSection in namedSections) { string[] values = namedSection.Select(p => p.propertyName).ToArray(); sections.Add(namedSection.Key, values); } return sections; } private static void FindAttributedSerializedFields(Type type, List> props) where TAttribute : PropertyAttribute { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); foreach (FieldInfo field in fields) { TAttribute attribute = field.GetCustomAttribute(); if (attribute == null) { continue; } if (field.GetCustomAttribute() == null) { continue; } props.Add(new AttributedProperty(field.Name, attribute)); } if (typeof(Component).IsAssignableFrom(type.BaseType)) { FindAttributedSerializedFields(type.BaseType, props); } } private struct AttributedProperty where TAttribute : PropertyAttribute { public string propertyName; public TAttribute attribute; public AttributedProperty(string propertyName, TAttribute attribute) { this.propertyName = propertyName; this.attribute = attribute; } } } }