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
}
}