SerializedDataEditorWindow.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. // Animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using System.Collections.Generic;
  5. using UnityEditor;
  6. using UnityEngine;
  7. using Object = UnityEngine.Object;
  8. namespace Animancer.Editor
  9. {
  10. /// <summary>[Editor-Only]
  11. /// A window for managing a copy of some serialized data and applying or reverting it.
  12. /// </summary>
  13. /// <remarks>
  14. /// This system assumes the implementation of <see cref="IEquatable{T}"/>
  15. /// compares the values of all fields in <typeparamref name="TData"/>.
  16. /// </remarks>
  17. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializedDataEditorWindow_2
  18. public abstract class SerializedDataEditorWindow<TObject, TData> : EditorWindow
  19. where TObject : Object
  20. where TData : class, ICopyable<TData>, IEquatable<TData>, new()
  21. {
  22. /************************************************************************************************************************/
  23. [SerializeField]
  24. private TObject _SourceObject;
  25. /// <summary>The object which contains the data this class manages.</summary>
  26. /// <remarks><see cref="SetAndCaptureSource"/> should generally be used instead of setting this property directly.</remarks>
  27. public virtual TObject SourceObject
  28. {
  29. get => _SourceObject;
  30. protected set => _SourceObject = value;
  31. }
  32. /************************************************************************************************************************/
  33. /// <summary>The <see cref="Data"/> field of the <see cref="SourceObject"/>.</summary>
  34. public abstract TData SourceData { get; set; }
  35. /************************************************************************************************************************/
  36. [SerializeField]
  37. private TData _Data;
  38. /// <summary>A copy of the <see cref="SourceData"/> being managed by this window.</summary>
  39. public ref TData Data
  40. => ref _Data;
  41. /************************************************************************************************************************/
  42. /// <summary>Is the <see cref="Data"/> managed by this window different to the <see cref="SourceData"/>.</summary>
  43. public bool HasDataChanged
  44. {
  45. get
  46. {
  47. try
  48. {
  49. return _Data != null && !_Data.Equals(SourceData);
  50. }
  51. catch (Exception exception)
  52. {
  53. Debug.LogException(exception);
  54. return false;
  55. }
  56. }
  57. }
  58. /************************************************************************************************************************/
  59. /// <summary>Initializes this window.</summary>
  60. protected virtual void OnEnable()
  61. {
  62. EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
  63. EditorApplication.wantsToQuit += OnTryCloseEditor;
  64. Undo.undoRedoPerformed += Repaint;
  65. }
  66. /// <summary>Cleans up this window.</summary>
  67. protected virtual void OnDisable()
  68. {
  69. EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
  70. EditorApplication.wantsToQuit -= OnTryCloseEditor;
  71. Undo.undoRedoPerformed -= Repaint;
  72. }
  73. /************************************************************************************************************************/
  74. /// <summary>
  75. /// Prompts the user to <see cref="Apply"/> or <see cref="Revert"/>
  76. /// if there are changes in the <see cref="Data"/> when this window is closed.
  77. /// </summary>
  78. protected virtual void OnDestroy()
  79. {
  80. var sourceObject = SourceObject;
  81. if (sourceObject == null ||
  82. !HasDataChanged ||
  83. titleContent == null)
  84. return;
  85. if (EditorUtility.DisplayDialog(
  86. titleContent.text,
  87. $"Apply unsaved changes to '{sourceObject.name}'?",
  88. "Apply",
  89. "Revert"))
  90. {
  91. Apply();
  92. }
  93. }
  94. /************************************************************************************************************************/
  95. /// <summary>Called before closing the Unity Editor to confirm that un-saved data is applied.</summary>
  96. private bool OnTryCloseEditor()
  97. {
  98. var sourceObject = SourceObject;
  99. if (sourceObject == null ||
  100. !HasDataChanged ||
  101. titleContent == null)
  102. return true;
  103. var option = EditorUtility.DisplayDialogComplex(
  104. titleContent.text,
  105. $"Apply unsaved changes to '{sourceObject.name}'?",
  106. "Apply",
  107. "Cancel",
  108. "Revert");
  109. switch (option)
  110. {
  111. case 0:// Apply.
  112. Apply();
  113. return true;
  114. case 2:// Revert.
  115. Revert();
  116. return true;
  117. case 1:// Cancel.
  118. default:
  119. return false;
  120. }
  121. }
  122. /************************************************************************************************************************/
  123. /// <summary>
  124. /// Sets the <see cref="SourceObject"/> and captures the <see cref="Data"/>
  125. /// as a copy of its <see cref="SourceData"/>.
  126. /// </summary>
  127. protected void SetAndCaptureSource(TObject sourceObject)
  128. {
  129. _SourceObject = sourceObject;
  130. CaptureData();
  131. Repaint();
  132. }
  133. /************************************************************************************************************************/
  134. /// <summary>
  135. /// Override this to return <c>true</c> if the <see cref="SourceObject"/> could be part of a prefab
  136. /// to ensure that modifications are serialized properly.
  137. /// </summary>
  138. public virtual bool SourceObjectMightBePrefab
  139. => false;
  140. /************************************************************************************************************************/
  141. /// <summary>Saves the edited <see cref="Data"/> into the <see cref="SourceObject"/>.</summary>
  142. public virtual void Apply()
  143. {
  144. var sourceObject = SourceObject;
  145. if (sourceObject == null)
  146. return;
  147. using (new ModifySerializedField(sourceObject, name, SourceObjectMightBePrefab))
  148. {
  149. SourceData = _Data.CopyableClone();
  150. if (EditorUtility.IsPersistent(SourceObject))
  151. {
  152. var objects = SetPool.Acquire<Object>();
  153. GatherObjectReferences(sourceObject, objects);
  154. foreach (var obj in objects)
  155. if (!EditorUtility.IsPersistent(obj))
  156. AssetDatabase.AddObjectToAsset(obj, SourceObject);
  157. SetPool.Release(objects);
  158. }
  159. }
  160. Repaint();
  161. AssetDatabase.SaveAssets();
  162. }
  163. /************************************************************************************************************************/
  164. /// <summary>Gathers all objects referenced by the `root`.</summary>
  165. public static void GatherObjectReferences(Object root, HashSet<Object> objects)
  166. {
  167. using var serializedObject = new SerializedObject(root);
  168. var property = serializedObject.GetIterator();
  169. while (property.Next(true))
  170. {
  171. if (property.propertyType == SerializedPropertyType.ObjectReference)
  172. {
  173. var value = property.objectReferenceValue;
  174. if (value != null)
  175. objects.Add(value);
  176. }
  177. }
  178. }
  179. /************************************************************************************************************************/
  180. /// <summary>Restores the <see cref="Data"/> to the original values from the <see cref="SourceData"/>.</summary>
  181. public virtual void Revert()
  182. {
  183. RecordUndo();
  184. CaptureData();
  185. }
  186. /************************************************************************************************************************/
  187. /// <summary>Stores a copy of the <see cref="SourceData"/> in the <see cref="Data"/>.</summary>
  188. protected virtual void CaptureData()
  189. {
  190. _Data = SourceData?.CopyableClone() ?? new();
  191. AnimancerReflection.TryInvoke(_Data, "OnValidate");
  192. }
  193. /************************************************************************************************************************/
  194. /// <summary>Records the current state of this window so it can be undone later.</summary>
  195. public TData RecordUndo()
  196. => RecordUndo(titleContent.text);
  197. /// <summary>Records the current state of this window so it can be undone later.</summary>
  198. public virtual TData RecordUndo(string name)
  199. {
  200. Undo.RecordObject(this, name);
  201. Repaint();
  202. return _Data;
  203. }
  204. /************************************************************************************************************************/
  205. /// <summary>
  206. /// Opens a new <typeparamref name="TWindow"/> for the `sourceObject`
  207. /// or gives focus to an existing window that was already displaying it.
  208. /// </summary>
  209. public static TWindow Open<TWindow>(
  210. TObject sourceObject,
  211. bool onlyOneWindow = false,
  212. params Type[] desiredDockNextTo)
  213. where TWindow : SerializedDataEditorWindow<TObject, TData>
  214. {
  215. if (!onlyOneWindow)
  216. {
  217. foreach (var window in Resources.FindObjectsOfTypeAll<TWindow>())
  218. {
  219. if (window.SourceObject == sourceObject)
  220. {
  221. window.Show();
  222. window.SetAndCaptureSource(sourceObject);
  223. window.Focus();
  224. return window;
  225. }
  226. }
  227. }
  228. var newWindow = onlyOneWindow
  229. ? GetWindow<TWindow>(desiredDockNextTo ?? Type.EmptyTypes)
  230. : CreateInstance<TWindow>();
  231. newWindow.Show();
  232. newWindow.SetAndCaptureSource(sourceObject);
  233. return newWindow;
  234. }
  235. /************************************************************************************************************************/
  236. #region Auto Apply
  237. /************************************************************************************************************************/
  238. /// <summary>The <see cref="EditorPrefs"/> key for <see cref="AutoApply"/>.</summary>
  239. protected virtual string AutoApplyPref
  240. => $"{titleContent.text}.{nameof(AutoApply)}";
  241. /************************************************************************************************************************/
  242. private bool _HasLoadedAutoApply;
  243. private bool _AutoApply;
  244. private bool _EnabledAutoApplyInPlayMode;
  245. /// <summary>Is the "Auto Apply" toggle currently enabled?</summary>
  246. public bool AutoApply
  247. {
  248. get
  249. {
  250. if (!_HasLoadedAutoApply)
  251. {
  252. _HasLoadedAutoApply = true;
  253. _AutoApply = EditorPrefs.GetBool(AutoApplyPref);
  254. }
  255. return _AutoApply;
  256. }
  257. set
  258. {
  259. _HasLoadedAutoApply = true;
  260. _AutoApply = value;
  261. _EnabledAutoApplyInPlayMode = _AutoApply && EditorApplication.isPlayingOrWillChangePlaymode;
  262. EditorPrefs.SetBool(AutoApplyPref, value);
  263. }
  264. }
  265. /************************************************************************************************************************/
  266. /// <summary>Handles entering and exiting Play Mode.</summary>
  267. protected virtual void OnPlayModeStateChanged(PlayModeStateChange change)
  268. {
  269. switch (change)
  270. {
  271. case PlayModeStateChange.EnteredPlayMode:
  272. if (HasDataChanged && focusedWindow != null)
  273. focusedWindow.ShowNotification(new($"{titleContent.text} window has un-applied changes"));
  274. break;
  275. case PlayModeStateChange.ExitingPlayMode:
  276. if (_EnabledAutoApplyInPlayMode)
  277. AutoApply = false;
  278. break;
  279. }
  280. }
  281. /************************************************************************************************************************/
  282. #endregion
  283. /************************************************************************************************************************/
  284. #region GUI
  285. /************************************************************************************************************************/
  286. private static readonly GUIContent
  287. RevertLabel = new(
  288. "Revert",
  289. "Undo all changes made in this window"),
  290. ApplyLabel = new(
  291. "Apply",
  292. "Apply all changes made in this window to the source object"),
  293. AutoApplyLabel = new(
  294. "Auto",
  295. "Immediately apply all changes made in this window to the source object?" +
  296. "\n\nIf enabled in Play Mode, this toggle will be disabled when returning to Edit Mode.");
  297. /************************************************************************************************************************/
  298. /// <summary>
  299. /// Calculates the pixel width required for
  300. /// <see cref="DoApplyRevertGUI(Rect, Rect, Rect, ButtonGroupStyles)"/>.
  301. /// </summary>
  302. public float CalculateApplyRevertWidth(ButtonGroupStyles styles = default)
  303. {
  304. styles.CopyMissingStyles(ButtonGroupStyles.Button);
  305. return
  306. styles.left.CalculateWidth(RevertLabel) +
  307. styles.middle.CalculateWidth(ApplyLabel) +
  308. styles.right.CalculateWidth(AutoApplyLabel);
  309. }
  310. /************************************************************************************************************************/
  311. /// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
  312. public void DoApplyRevertGUI(ButtonGroupStyles styles = default)
  313. {
  314. styles.CopyMissingStyles(ButtonGroupStyles.Button);
  315. GUILayout.BeginHorizontal();
  316. var leftArea = GUILayoutUtility.GetRect(RevertLabel, styles.left);
  317. var middleArea = GUILayoutUtility.GetRect(ApplyLabel, styles.middle);
  318. var rightArea = GUILayoutUtility.GetRect(AutoApplyLabel, styles.right);
  319. DoApplyRevertGUI(leftArea, middleArea, rightArea, styles);
  320. GUILayout.EndHorizontal();
  321. }
  322. /************************************************************************************************************************/
  323. /// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
  324. public void DoApplyRevertGUI(Rect area, ButtonGroupStyles styles = default)
  325. {
  326. styles.CopyMissingStyles(ButtonGroupStyles.Button);
  327. var leftArea = AnimancerGUI.StealFromLeft(ref area, styles.left.CalculateWidth(RevertLabel));
  328. var middleArea = AnimancerGUI.StealFromLeft(ref area, styles.middle.CalculateWidth(ApplyLabel));
  329. DoApplyRevertGUI(leftArea, middleArea, area, styles);
  330. }
  331. /************************************************************************************************************************/
  332. /// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
  333. public void DoApplyRevertGUI(
  334. Rect leftArea,
  335. Rect middleArea,
  336. Rect rightArea,
  337. ButtonGroupStyles styles = default)
  338. {
  339. styles.CopyMissingStyles(ButtonGroupStyles.Button);
  340. var enabled = GUI.enabled;
  341. GUI.enabled = SourceObject != null && HasDataChanged;
  342. // Revert.
  343. if (GUI.Button(leftArea, RevertLabel, styles.left))
  344. Revert();
  345. // Apply.
  346. if (GUI.Button(middleArea, ApplyLabel, styles.middle))
  347. Apply();
  348. // Auto Apply.
  349. var autoApply = AutoApply;
  350. if (autoApply && GUI.enabled)
  351. Apply();
  352. GUI.enabled = enabled;
  353. if (autoApply != GUI.Toggle(rightArea, autoApply, AutoApplyLabel, styles.right))
  354. AutoApply = !autoApply;
  355. }
  356. /************************************************************************************************************************/
  357. #endregion
  358. /************************************************************************************************************************/
  359. }
  360. }
  361. #endif