using System; using System.Linq; using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; using UnityEditor; namespace Rokid.UXR.Editor { /// /// A utility class for building custom editors with less work required. /// public class EditorBase : UnityEditor.Editor { #region API protected virtual void OnEnable() { } protected virtual void OnDisable() { } /// /// You must put all of the editor specifications into OnInit /// protected virtual void OnInit() { } /// /// Call in OnInit with one or more property names to hide them from the inspector. /// /// This is preferable to using [HideInInspector] because it still allows the property to /// be viewed when using the Inspector debug mode. /// protected void Hide(params string[] properties) { Assert.IsTrue(properties.Length > 0, "Should always hide at least one property."); if (!ValidateProperties(properties)) { return; } _hiddenProperties.UnionWith(properties); } /// /// Call in OnInit with one or more property names to defer drawing them until after all /// non-deferred properties have been drawn. All deferred properties will be drawn in the order /// they are passed in to calls to Defer. /// protected void Defer(params string[] properties) { Assert.IsTrue(properties.Length > 0, "Should always defer at least one property."); if (!ValidateProperties(properties)) { return; } foreach (var property in properties) { if (_deferredProperties.Contains(property)) { continue; } _deferredProperties.Add(property); _deferredActions.Add(() => { DrawProperty(serializedObject.FindProperty(property)); }); } } /// /// Call in OnInit with a single property name and a custom property drawer. Equivalent /// to calling Draw and then Defer for the property. /// protected void Defer(string property, Action customDrawer) { Draw(property, customDrawer); Defer(property); } /// /// Call in OnInit with a single delegate to have it be called after all other non-deferred /// properties have been drawn. /// protected void Defer(Action deferredAction) { _deferredActions.Add(deferredAction); } /// /// Call in OnInit to specify a custom drawer for a single property. Whenever the property is drawn, /// it will use the provided property drawer instead of the default one. /// protected void Draw(string property, Action drawer) { if (!ValidateProperties(property)) { return; } _customDrawers.Add(property, drawer); } /// /// Call in OnInit to specify a custom drawer for a single property. Include an extra property that gets /// lumped in with the primary property. The extra property is not drawn normally, and is instead grouped in /// with the primary property. Can be used in situations where a collection of properties need to be drawn together. /// protected void Draw(string property, string withExtra0, Action drawer) { if (!ValidateProperties(property, withExtra0)) { return; } Hide(withExtra0); Draw(property, p => { drawer(p, serializedObject.FindProperty(withExtra0)); }); } protected void Draw(string property, string withExtra0, string withExtra1, Action drawer) { if (!ValidateProperties(property, withExtra0, withExtra1)) { return; } Hide(withExtra0); Hide(withExtra1); Draw(property, p => { drawer(p, serializedObject.FindProperty(withExtra0), serializedObject.FindProperty(withExtra1)); }); } protected void Draw(string property, string withExtra0, string withExtra1, string withExtra2, Action drawer) { if (!ValidateProperties(property, withExtra0, withExtra1, withExtra2)) { return; } Hide(withExtra0); Hide(withExtra1); Hide(withExtra2); Draw(property, p => { drawer(p, serializedObject.FindProperty(withExtra0), serializedObject.FindProperty(withExtra1), serializedObject.FindProperty(withExtra2)); }); } protected void Draw(string property, string withExtra0, string withExtra1, string withExtra2, string withExtra3, Action drawer) { if (!ValidateProperties(property, withExtra0, withExtra1, withExtra2, withExtra3)) { return; } Hide(withExtra0); Hide(withExtra1); Hide(withExtra2); Hide(withExtra3); Draw(property, p => { drawer(p, serializedObject.FindProperty(withExtra0), serializedObject.FindProperty(withExtra1), serializedObject.FindProperty(withExtra2), serializedObject.FindProperty(withExtra3)); }); } protected void Conditional(string boolPropName, bool showIf, params string[] toHide) { if (!ValidateProperties(boolPropName) || !ValidateProperties(toHide)) { return; } var boolProp = serializedObject.FindProperty(boolPropName); if (boolProp.propertyType != SerializedPropertyType.Boolean) { Debug.LogError( $"Must provide a Boolean property to this Conditional method, but the property {boolPropName} had a type of {boolProp.propertyType}"); return; } List> conditions; foreach (var prop in toHide) { if (!_propertyDrawConditions.TryGetValue(prop, out conditions)) { conditions = new List>(); _propertyDrawConditions[prop] = conditions; } conditions.Add(() => { if (boolProp.hasMultipleDifferentValues) { return false; } else { return boolProp.boolValue == showIf; } }); } } protected void Conditional(string enumPropName, T showIf, params string[] toHide) where T : Enum { if (!ValidateProperties(enumPropName) || !ValidateProperties(toHide)) { return; } var enumProp = serializedObject.FindProperty(enumPropName); if (enumProp.propertyType != SerializedPropertyType.Enum) { Debug.LogError( $"Must provide a Boolean property to this Conditional method, but the property {enumPropName} had a type of {enumProp.propertyType}"); return; } List> conditions; foreach (var prop in toHide) { if (!_propertyDrawConditions.TryGetValue(prop, out conditions)) { conditions = new List>(); _propertyDrawConditions[prop] = conditions; } conditions.Add(() => { if (enumProp.hasMultipleDifferentValues) { return false; } else { return enumProp.intValue == showIf.GetHashCode(); } }); } } /// /// Call in OnInit to specify a custom decorator for a single property. Before a property is drawn, /// all of the decorators will be drawn first. /// protected void Decorate(string property, Action decorator) { if (!ValidateProperties(property)) { return; } List> decorators; if (!_customDecorators.TryGetValue(property, out decorators)) { decorators = new List>(); _customDecorators[property] = decorators; } decorators.Add(decorator); } /// /// Call in OnInit to specify a custom grouping behaviour for a range of properties. Specify the first /// and last property (inclusive) and the action to take BEFORE the first property is drawn, and the action /// to take AFTER the last property is drawn. /// protected void Group(string firstProperty, string lastProperty, Action beginGroup, Action endGroup) { if (!ValidateProperties(firstProperty) || !ValidateProperties(lastProperty)) { return; } _groupBegins.Add(firstProperty, beginGroup); _groupEnds.Add(lastProperty, endGroup); } /// /// A utility version of the more generic Group method. /// Call in OnInit to specify a range of properties that should be grouped within a styled vertical /// layout group. /// protected void Group(string firstProperty, string lastProperty, GUIStyle style) { if (style == null) { Debug.LogError( "Cannot provide a null style to EditorBase.Group. If you are acquiring a " + "Style from the EditorStyles class, try calling Group from with on OnInit instead " + "of from within OnEnable."); return; } Group(firstProperty, lastProperty, () => EditorGUILayout.BeginVertical(style), () => EditorGUILayout.EndVertical()); } /// /// Groups the given properties into a foldout with a given name. /// protected void Foldout(string firstProperty, string lastProperty, string foldoutName, bool showByDefault = false) { Group(firstProperty, lastProperty, () => { bool shouldShow; if (!_foldouts.TryGetValue(foldoutName, out shouldShow)) { shouldShow = showByDefault; } shouldShow = EditorGUILayout.Foldout(shouldShow, foldoutName); _foldouts[foldoutName] = shouldShow; EditorGUI.indentLevel++; _currentStates.Push(shouldShow); }, () => { EditorGUI.indentLevel--; _currentStates.Pop(); }); } protected virtual void OnBeforeInspector() { } protected virtual void OnAfterInspector(bool anyPropertiesModified) { } #endregion #region IMPLEMENTATION [NonSerialized] private bool _hasInitBeenCalled = false; private HashSet _hiddenProperties = new HashSet(); private HashSet _deferredProperties = new HashSet(); private List _deferredActions = new List(); private Dictionary _foldouts = new Dictionary(); private Stack _currentStates = new Stack(); private Dictionary> _customDrawers = new Dictionary>(); private Dictionary>> _customDecorators = new Dictionary>>(); private Dictionary _groupBegins = new Dictionary(); private Dictionary _groupEnds = new Dictionary(); private Dictionary>> _propertyDrawConditions = new Dictionary>>(); public override void OnInspectorGUI() { if (!_hasInitBeenCalled) { OnInit(); _hasInitBeenCalled = true; } SerializedProperty it = serializedObject.GetIterator(); it.NextVisible(enterChildren: true); //Draw script header EditorGUI.BeginDisabledGroup(true); EditorGUILayout.PropertyField(it); EditorGUI.EndDisabledGroup(); OnBeforeInspector(); EditorGUI.BeginChangeCheck(); while (it.NextVisible(enterChildren: false)) { //Don't draw deferred properties in this pass, we will draw them after everything else if (_deferredProperties.Contains(it.name)) { continue; } DrawProperty(it); } foreach (var deferredAction in _deferredActions) { deferredAction(); } bool anyModified = EditorGUI.EndChangeCheck(); OnAfterInspector(anyModified); serializedObject.ApplyModifiedProperties(); } private void DrawProperty(SerializedProperty property) { Action groupBeginAction; if (_groupBegins.TryGetValue(property.name, out groupBeginAction)) { groupBeginAction(); } try { //Don't draw if we are in a property that is currently hidden by a foldout if (_currentStates.Any(s => s == false)) { return; } //Don't draw hidden properties if (_hiddenProperties.Contains(property.name)) { return; } List> conditions; if (_propertyDrawConditions.TryGetValue(property.name, out conditions)) { foreach (var condition in conditions) { if (!condition()) { return; } } } //First draw all decorators for the property List> decorators; if (_customDecorators.TryGetValue(property.name, out decorators)) { foreach (var decorator in decorators) { decorator(property); } } //Then draw the property itself, using a custom drawer if needed Action customDrawer; if (_customDrawers.TryGetValue(property.name, out customDrawer)) { customDrawer(property); } else { EditorGUILayout.PropertyField(property, includeChildren: true); } } finally { Action groupEndAction; if (_groupEnds.TryGetValue(property.name, out groupEndAction)) { groupEndAction(); } } } 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 } }