EditorBase.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. using System;
  2. using System.Linq;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. using UnityEngine.Assertions;
  6. using UnityEditor;
  7. namespace Rokid.UXR.Editor
  8. {
  9. /// <summary>
  10. /// A utility class for building custom editors with less work required.
  11. /// </summary>
  12. public class EditorBase : UnityEditor.Editor
  13. {
  14. #region API
  15. protected virtual void OnEnable() { }
  16. protected virtual void OnDisable() { }
  17. /// <summary>
  18. /// You must put all of the editor specifications into OnInit
  19. /// </summary>
  20. protected virtual void OnInit() { }
  21. /// <summary>
  22. /// Call in OnInit with one or more property names to hide them from the inspector.
  23. ///
  24. /// This is preferable to using [HideInInspector] because it still allows the property to
  25. /// be viewed when using the Inspector debug mode.
  26. /// </summary>
  27. protected void Hide(params string[] properties)
  28. {
  29. Assert.IsTrue(properties.Length > 0, "Should always hide at least one property.");
  30. if (!ValidateProperties(properties))
  31. {
  32. return;
  33. }
  34. _hiddenProperties.UnionWith(properties);
  35. }
  36. /// <summary>
  37. /// Call in OnInit with one or more property names to defer drawing them until after all
  38. /// non-deferred properties have been drawn. All deferred properties will be drawn in the order
  39. /// they are passed in to calls to Defer.
  40. /// </summary>
  41. protected void Defer(params string[] properties)
  42. {
  43. Assert.IsTrue(properties.Length > 0, "Should always defer at least one property.");
  44. if (!ValidateProperties(properties))
  45. {
  46. return;
  47. }
  48. foreach (var property in properties)
  49. {
  50. if (_deferredProperties.Contains(property))
  51. {
  52. continue;
  53. }
  54. _deferredProperties.Add(property);
  55. _deferredActions.Add(() =>
  56. {
  57. DrawProperty(serializedObject.FindProperty(property));
  58. });
  59. }
  60. }
  61. /// <summary>
  62. /// Call in OnInit with a single property name and a custom property drawer. Equivalent
  63. /// to calling Draw and then Defer for the property.
  64. /// </summary>
  65. protected void Defer(string property, Action<SerializedProperty> customDrawer)
  66. {
  67. Draw(property, customDrawer);
  68. Defer(property);
  69. }
  70. /// <summary>
  71. /// Call in OnInit with a single delegate to have it be called after all other non-deferred
  72. /// properties have been drawn.
  73. /// </summary>
  74. protected void Defer(Action deferredAction)
  75. {
  76. _deferredActions.Add(deferredAction);
  77. }
  78. /// <summary>
  79. /// Call in OnInit to specify a custom drawer for a single property. Whenever the property is drawn,
  80. /// it will use the provided property drawer instead of the default one.
  81. /// </summary>
  82. protected void Draw(string property, Action<SerializedProperty> drawer)
  83. {
  84. if (!ValidateProperties(property))
  85. {
  86. return;
  87. }
  88. _customDrawers.Add(property, drawer);
  89. }
  90. /// <summary>
  91. /// Call in OnInit to specify a custom drawer for a single property. Include an extra property that gets
  92. /// lumped in with the primary property. The extra property is not drawn normally, and is instead grouped in
  93. /// with the primary property. Can be used in situations where a collection of properties need to be drawn together.
  94. /// </summary>
  95. protected void Draw(string property,
  96. string withExtra0,
  97. Action<SerializedProperty, SerializedProperty> drawer)
  98. {
  99. if (!ValidateProperties(property, withExtra0))
  100. {
  101. return;
  102. }
  103. Hide(withExtra0);
  104. Draw(property, p =>
  105. {
  106. drawer(p,
  107. serializedObject.FindProperty(withExtra0));
  108. });
  109. }
  110. protected void Draw(string property,
  111. string withExtra0,
  112. string withExtra1,
  113. Action<SerializedProperty, SerializedProperty, SerializedProperty> drawer)
  114. {
  115. if (!ValidateProperties(property, withExtra0, withExtra1))
  116. {
  117. return;
  118. }
  119. Hide(withExtra0);
  120. Hide(withExtra1);
  121. Draw(property, p =>
  122. {
  123. drawer(p,
  124. serializedObject.FindProperty(withExtra0),
  125. serializedObject.FindProperty(withExtra1));
  126. });
  127. }
  128. protected void Draw(string property,
  129. string withExtra0,
  130. string withExtra1,
  131. string withExtra2,
  132. Action<SerializedProperty, SerializedProperty, SerializedProperty, SerializedProperty>
  133. drawer)
  134. {
  135. if (!ValidateProperties(property, withExtra0, withExtra1, withExtra2))
  136. {
  137. return;
  138. }
  139. Hide(withExtra0);
  140. Hide(withExtra1);
  141. Hide(withExtra2);
  142. Draw(property, p =>
  143. {
  144. drawer(p,
  145. serializedObject.FindProperty(withExtra0),
  146. serializedObject.FindProperty(withExtra1),
  147. serializedObject.FindProperty(withExtra2));
  148. });
  149. }
  150. protected void Draw(string property,
  151. string withExtra0,
  152. string withExtra1,
  153. string withExtra2,
  154. string withExtra3,
  155. Action<SerializedProperty, SerializedProperty, SerializedProperty, SerializedProperty,
  156. SerializedProperty> drawer)
  157. {
  158. if (!ValidateProperties(property, withExtra0, withExtra1, withExtra2, withExtra3))
  159. {
  160. return;
  161. }
  162. Hide(withExtra0);
  163. Hide(withExtra1);
  164. Hide(withExtra2);
  165. Hide(withExtra3);
  166. Draw(property, p =>
  167. {
  168. drawer(p,
  169. serializedObject.FindProperty(withExtra0),
  170. serializedObject.FindProperty(withExtra1),
  171. serializedObject.FindProperty(withExtra2),
  172. serializedObject.FindProperty(withExtra3));
  173. });
  174. }
  175. protected void Conditional(string boolPropName, bool showIf, params string[] toHide)
  176. {
  177. if (!ValidateProperties(boolPropName) || !ValidateProperties(toHide))
  178. {
  179. return;
  180. }
  181. var boolProp = serializedObject.FindProperty(boolPropName);
  182. if (boolProp.propertyType != SerializedPropertyType.Boolean)
  183. {
  184. Debug.LogError(
  185. $"Must provide a Boolean property to this Conditional method, but the property {boolPropName} had a type of {boolProp.propertyType}");
  186. return;
  187. }
  188. List<Func<bool>> conditions;
  189. foreach (var prop in toHide)
  190. {
  191. if (!_propertyDrawConditions.TryGetValue(prop, out conditions))
  192. {
  193. conditions = new List<Func<bool>>();
  194. _propertyDrawConditions[prop] = conditions;
  195. }
  196. conditions.Add(() =>
  197. {
  198. if (boolProp.hasMultipleDifferentValues)
  199. {
  200. return false;
  201. }
  202. else
  203. {
  204. return boolProp.boolValue == showIf;
  205. }
  206. });
  207. }
  208. }
  209. protected void Conditional<T>(string enumPropName, T showIf, params string[] toHide)
  210. where T : Enum
  211. {
  212. if (!ValidateProperties(enumPropName) || !ValidateProperties(toHide))
  213. {
  214. return;
  215. }
  216. var enumProp = serializedObject.FindProperty(enumPropName);
  217. if (enumProp.propertyType != SerializedPropertyType.Enum)
  218. {
  219. Debug.LogError(
  220. $"Must provide a Boolean property to this Conditional method, but the property {enumPropName} had a type of {enumProp.propertyType}");
  221. return;
  222. }
  223. List<Func<bool>> conditions;
  224. foreach (var prop in toHide)
  225. {
  226. if (!_propertyDrawConditions.TryGetValue(prop, out conditions))
  227. {
  228. conditions = new List<Func<bool>>();
  229. _propertyDrawConditions[prop] = conditions;
  230. }
  231. conditions.Add(() =>
  232. {
  233. if (enumProp.hasMultipleDifferentValues)
  234. {
  235. return false;
  236. }
  237. else
  238. {
  239. return enumProp.intValue == showIf.GetHashCode();
  240. }
  241. });
  242. }
  243. }
  244. /// <summary>
  245. /// Call in OnInit to specify a custom decorator for a single property. Before a property is drawn,
  246. /// all of the decorators will be drawn first.
  247. /// </summary>
  248. protected void Decorate(string property, Action<SerializedProperty> decorator)
  249. {
  250. if (!ValidateProperties(property))
  251. {
  252. return;
  253. }
  254. List<Action<SerializedProperty>> decorators;
  255. if (!_customDecorators.TryGetValue(property, out decorators))
  256. {
  257. decorators = new List<Action<SerializedProperty>>();
  258. _customDecorators[property] = decorators;
  259. }
  260. decorators.Add(decorator);
  261. }
  262. /// <summary>
  263. /// Call in OnInit to specify a custom grouping behaviour for a range of properties. Specify the first
  264. /// and last property (inclusive) and the action to take BEFORE the first property is drawn, and the action
  265. /// to take AFTER the last property is drawn.
  266. /// </summary>
  267. protected void Group(string firstProperty, string lastProperty, Action beginGroup,
  268. Action endGroup)
  269. {
  270. if (!ValidateProperties(firstProperty) || !ValidateProperties(lastProperty))
  271. {
  272. return;
  273. }
  274. _groupBegins.Add(firstProperty, beginGroup);
  275. _groupEnds.Add(lastProperty, endGroup);
  276. }
  277. /// <summary>
  278. /// A utility version of the more generic Group method.
  279. /// Call in OnInit to specify a range of properties that should be grouped within a styled vertical
  280. /// layout group.
  281. /// </summary>
  282. protected void Group(string firstProperty, string lastProperty, GUIStyle style)
  283. {
  284. if (style == null)
  285. {
  286. Debug.LogError(
  287. "Cannot provide a null style to EditorBase.Group. If you are acquiring a " +
  288. "Style from the EditorStyles class, try calling Group from with on OnInit instead " +
  289. "of from within OnEnable.");
  290. return;
  291. }
  292. Group(firstProperty,
  293. lastProperty,
  294. () => EditorGUILayout.BeginVertical(style),
  295. () => EditorGUILayout.EndVertical());
  296. }
  297. /// <summary>
  298. /// Groups the given properties into a foldout with a given name.
  299. /// </summary>
  300. protected void Foldout(string firstProperty, string lastProperty, string foldoutName,
  301. bool showByDefault = false)
  302. {
  303. Group(firstProperty,
  304. lastProperty,
  305. () =>
  306. {
  307. bool shouldShow;
  308. if (!_foldouts.TryGetValue(foldoutName, out shouldShow))
  309. {
  310. shouldShow = showByDefault;
  311. }
  312. shouldShow = EditorGUILayout.Foldout(shouldShow, foldoutName);
  313. _foldouts[foldoutName] = shouldShow;
  314. EditorGUI.indentLevel++;
  315. _currentStates.Push(shouldShow);
  316. },
  317. () =>
  318. {
  319. EditorGUI.indentLevel--;
  320. _currentStates.Pop();
  321. });
  322. }
  323. protected virtual void OnBeforeInspector() { }
  324. protected virtual void OnAfterInspector(bool anyPropertiesModified) { }
  325. #endregion
  326. #region IMPLEMENTATION
  327. [NonSerialized]
  328. private bool _hasInitBeenCalled = false;
  329. private HashSet<string> _hiddenProperties = new HashSet<string>();
  330. private HashSet<string> _deferredProperties = new HashSet<string>();
  331. private List<Action> _deferredActions = new List<Action>();
  332. private Dictionary<string, bool> _foldouts = new Dictionary<string, bool>();
  333. private Stack<bool> _currentStates = new Stack<bool>();
  334. private Dictionary<string, Action<SerializedProperty>> _customDrawers =
  335. new Dictionary<string, Action<SerializedProperty>>();
  336. private Dictionary<string, List<Action<SerializedProperty>>> _customDecorators =
  337. new Dictionary<string, List<Action<SerializedProperty>>>();
  338. private Dictionary<string, Action> _groupBegins = new Dictionary<string, Action>();
  339. private Dictionary<string, Action> _groupEnds = new Dictionary<string, Action>();
  340. private Dictionary<string, List<Func<bool>>> _propertyDrawConditions =
  341. new Dictionary<string, List<Func<bool>>>();
  342. public override void OnInspectorGUI()
  343. {
  344. if (!_hasInitBeenCalled)
  345. {
  346. OnInit();
  347. _hasInitBeenCalled = true;
  348. }
  349. SerializedProperty it = serializedObject.GetIterator();
  350. it.NextVisible(enterChildren: true);
  351. //Draw script header
  352. EditorGUI.BeginDisabledGroup(true);
  353. EditorGUILayout.PropertyField(it);
  354. EditorGUI.EndDisabledGroup();
  355. OnBeforeInspector();
  356. EditorGUI.BeginChangeCheck();
  357. while (it.NextVisible(enterChildren: false))
  358. {
  359. //Don't draw deferred properties in this pass, we will draw them after everything else
  360. if (_deferredProperties.Contains(it.name))
  361. {
  362. continue;
  363. }
  364. DrawProperty(it);
  365. }
  366. foreach (var deferredAction in _deferredActions)
  367. {
  368. deferredAction();
  369. }
  370. bool anyModified = EditorGUI.EndChangeCheck();
  371. OnAfterInspector(anyModified);
  372. serializedObject.ApplyModifiedProperties();
  373. }
  374. private void DrawProperty(SerializedProperty property)
  375. {
  376. Action groupBeginAction;
  377. if (_groupBegins.TryGetValue(property.name, out groupBeginAction))
  378. {
  379. groupBeginAction();
  380. }
  381. try
  382. {
  383. //Don't draw if we are in a property that is currently hidden by a foldout
  384. if (_currentStates.Any(s => s == false))
  385. {
  386. return;
  387. }
  388. //Don't draw hidden properties
  389. if (_hiddenProperties.Contains(property.name))
  390. {
  391. return;
  392. }
  393. List<Func<bool>> conditions;
  394. if (_propertyDrawConditions.TryGetValue(property.name, out conditions))
  395. {
  396. foreach (var condition in conditions)
  397. {
  398. if (!condition())
  399. {
  400. return;
  401. }
  402. }
  403. }
  404. //First draw all decorators for the property
  405. List<Action<SerializedProperty>> decorators;
  406. if (_customDecorators.TryGetValue(property.name, out decorators))
  407. {
  408. foreach (var decorator in decorators)
  409. {
  410. decorator(property);
  411. }
  412. }
  413. //Then draw the property itself, using a custom drawer if needed
  414. Action<SerializedProperty> customDrawer;
  415. if (_customDrawers.TryGetValue(property.name, out customDrawer))
  416. {
  417. customDrawer(property);
  418. }
  419. else
  420. {
  421. EditorGUILayout.PropertyField(property, includeChildren: true);
  422. }
  423. }
  424. finally
  425. {
  426. Action groupEndAction;
  427. if (_groupEnds.TryGetValue(property.name, out groupEndAction))
  428. {
  429. groupEndAction();
  430. }
  431. }
  432. }
  433. private bool ValidateProperties(params string[] properties)
  434. {
  435. foreach (var property in properties)
  436. {
  437. if (serializedObject.FindProperty(property) == null)
  438. {
  439. Debug.LogWarning(
  440. $"Could not find property {property}, maybe it was deleted or renamed?");
  441. return false;
  442. }
  443. }
  444. return true;
  445. }
  446. #endregion
  447. }
  448. }