// Animancer // Copyright 2018-2024 Kybernetik // #if UNITY_EDITOR using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace Animancer.Editor { /// [Editor-Only] /// A window for managing a copy of some serialized data and applying or reverting it. /// /// /// This system assumes the implementation of /// compares the values of all fields in . /// /// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializedDataEditorWindow_2 public abstract class SerializedDataEditorWindow : EditorWindow where TObject : Object where TData : class, ICopyable, IEquatable, new() { /************************************************************************************************************************/ [SerializeField] private TObject _SourceObject; /// The object which contains the data this class manages. /// should generally be used instead of setting this property directly. public virtual TObject SourceObject { get => _SourceObject; protected set => _SourceObject = value; } /************************************************************************************************************************/ /// The field of the . public abstract TData SourceData { get; set; } /************************************************************************************************************************/ [SerializeField] private TData _Data; /// A copy of the being managed by this window. public ref TData Data => ref _Data; /************************************************************************************************************************/ /// Is the managed by this window different to the . public bool HasDataChanged { get { try { return _Data != null && !_Data.Equals(SourceData); } catch (Exception exception) { Debug.LogException(exception); return false; } } } /************************************************************************************************************************/ /// Initializes this window. protected virtual void OnEnable() { EditorApplication.playModeStateChanged += OnPlayModeStateChanged; EditorApplication.wantsToQuit += OnTryCloseEditor; Undo.undoRedoPerformed += Repaint; } /// Cleans up this window. protected virtual void OnDisable() { EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; EditorApplication.wantsToQuit -= OnTryCloseEditor; Undo.undoRedoPerformed -= Repaint; } /************************************************************************************************************************/ /// /// Prompts the user to or /// if there are changes in the when this window is closed. /// protected virtual void OnDestroy() { var sourceObject = SourceObject; if (sourceObject == null || !HasDataChanged || titleContent == null) return; if (EditorUtility.DisplayDialog( titleContent.text, $"Apply unsaved changes to '{sourceObject.name}'?", "Apply", "Revert")) { Apply(); } } /************************************************************************************************************************/ /// Called before closing the Unity Editor to confirm that un-saved data is applied. private bool OnTryCloseEditor() { var sourceObject = SourceObject; if (sourceObject == null || !HasDataChanged || titleContent == null) return true; var option = EditorUtility.DisplayDialogComplex( titleContent.text, $"Apply unsaved changes to '{sourceObject.name}'?", "Apply", "Cancel", "Revert"); switch (option) { case 0:// Apply. Apply(); return true; case 2:// Revert. Revert(); return true; case 1:// Cancel. default: return false; } } /************************************************************************************************************************/ /// /// Sets the and captures the /// as a copy of its . /// protected void SetAndCaptureSource(TObject sourceObject) { _SourceObject = sourceObject; CaptureData(); Repaint(); } /************************************************************************************************************************/ /// /// Override this to return true if the could be part of a prefab /// to ensure that modifications are serialized properly. /// public virtual bool SourceObjectMightBePrefab => false; /************************************************************************************************************************/ /// Saves the edited into the . public virtual void Apply() { var sourceObject = SourceObject; if (sourceObject == null) return; using (new ModifySerializedField(sourceObject, name, SourceObjectMightBePrefab)) { SourceData = _Data.CopyableClone(); if (EditorUtility.IsPersistent(SourceObject)) { var objects = SetPool.Acquire(); GatherObjectReferences(sourceObject, objects); foreach (var obj in objects) if (!EditorUtility.IsPersistent(obj)) AssetDatabase.AddObjectToAsset(obj, SourceObject); SetPool.Release(objects); } } Repaint(); AssetDatabase.SaveAssets(); } /************************************************************************************************************************/ /// Gathers all objects referenced by the `root`. public static void GatherObjectReferences(Object root, HashSet objects) { using var serializedObject = new SerializedObject(root); var property = serializedObject.GetIterator(); while (property.Next(true)) { if (property.propertyType == SerializedPropertyType.ObjectReference) { var value = property.objectReferenceValue; if (value != null) objects.Add(value); } } } /************************************************************************************************************************/ /// Restores the to the original values from the . public virtual void Revert() { RecordUndo(); CaptureData(); } /************************************************************************************************************************/ /// Stores a copy of the in the . protected virtual void CaptureData() { _Data = SourceData?.CopyableClone() ?? new(); AnimancerReflection.TryInvoke(_Data, "OnValidate"); } /************************************************************************************************************************/ /// Records the current state of this window so it can be undone later. public TData RecordUndo() => RecordUndo(titleContent.text); /// Records the current state of this window so it can be undone later. public virtual TData RecordUndo(string name) { Undo.RecordObject(this, name); Repaint(); return _Data; } /************************************************************************************************************************/ /// /// Opens a new for the `sourceObject` /// or gives focus to an existing window that was already displaying it. /// public static TWindow Open( TObject sourceObject, bool onlyOneWindow = false, params Type[] desiredDockNextTo) where TWindow : SerializedDataEditorWindow { if (!onlyOneWindow) { foreach (var window in Resources.FindObjectsOfTypeAll()) { if (window.SourceObject == sourceObject) { window.Show(); window.SetAndCaptureSource(sourceObject); window.Focus(); return window; } } } var newWindow = onlyOneWindow ? GetWindow(desiredDockNextTo ?? Type.EmptyTypes) : CreateInstance(); newWindow.Show(); newWindow.SetAndCaptureSource(sourceObject); return newWindow; } /************************************************************************************************************************/ #region Auto Apply /************************************************************************************************************************/ /// The key for . protected virtual string AutoApplyPref => $"{titleContent.text}.{nameof(AutoApply)}"; /************************************************************************************************************************/ private bool _HasLoadedAutoApply; private bool _AutoApply; private bool _EnabledAutoApplyInPlayMode; /// Is the "Auto Apply" toggle currently enabled? public bool AutoApply { get { if (!_HasLoadedAutoApply) { _HasLoadedAutoApply = true; _AutoApply = EditorPrefs.GetBool(AutoApplyPref); } return _AutoApply; } set { _HasLoadedAutoApply = true; _AutoApply = value; _EnabledAutoApplyInPlayMode = _AutoApply && EditorApplication.isPlayingOrWillChangePlaymode; EditorPrefs.SetBool(AutoApplyPref, value); } } /************************************************************************************************************************/ /// Handles entering and exiting Play Mode. protected virtual void OnPlayModeStateChanged(PlayModeStateChange change) { switch (change) { case PlayModeStateChange.EnteredPlayMode: if (HasDataChanged && focusedWindow != null) focusedWindow.ShowNotification(new($"{titleContent.text} window has un-applied changes")); break; case PlayModeStateChange.ExitingPlayMode: if (_EnabledAutoApplyInPlayMode) AutoApply = false; break; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region GUI /************************************************************************************************************************/ private static readonly GUIContent RevertLabel = new( "Revert", "Undo all changes made in this window"), ApplyLabel = new( "Apply", "Apply all changes made in this window to the source object"), AutoApplyLabel = new( "Auto", "Immediately apply all changes made in this window to the source object?" + "\n\nIf enabled in Play Mode, this toggle will be disabled when returning to Edit Mode."); /************************************************************************************************************************/ /// /// Calculates the pixel width required for /// . /// public float CalculateApplyRevertWidth(ButtonGroupStyles styles = default) { styles.CopyMissingStyles(ButtonGroupStyles.Button); return styles.left.CalculateWidth(RevertLabel) + styles.middle.CalculateWidth(ApplyLabel) + styles.right.CalculateWidth(AutoApplyLabel); } /************************************************************************************************************************/ /// Draws GUI controls for , , and . public void DoApplyRevertGUI(ButtonGroupStyles styles = default) { styles.CopyMissingStyles(ButtonGroupStyles.Button); GUILayout.BeginHorizontal(); var leftArea = GUILayoutUtility.GetRect(RevertLabel, styles.left); var middleArea = GUILayoutUtility.GetRect(ApplyLabel, styles.middle); var rightArea = GUILayoutUtility.GetRect(AutoApplyLabel, styles.right); DoApplyRevertGUI(leftArea, middleArea, rightArea, styles); GUILayout.EndHorizontal(); } /************************************************************************************************************************/ /// Draws GUI controls for , , and . public void DoApplyRevertGUI(Rect area, ButtonGroupStyles styles = default) { styles.CopyMissingStyles(ButtonGroupStyles.Button); var leftArea = AnimancerGUI.StealFromLeft(ref area, styles.left.CalculateWidth(RevertLabel)); var middleArea = AnimancerGUI.StealFromLeft(ref area, styles.middle.CalculateWidth(ApplyLabel)); DoApplyRevertGUI(leftArea, middleArea, area, styles); } /************************************************************************************************************************/ /// Draws GUI controls for , , and . public void DoApplyRevertGUI( Rect leftArea, Rect middleArea, Rect rightArea, ButtonGroupStyles styles = default) { styles.CopyMissingStyles(ButtonGroupStyles.Button); var enabled = GUI.enabled; GUI.enabled = SourceObject != null && HasDataChanged; // Revert. if (GUI.Button(leftArea, RevertLabel, styles.left)) Revert(); // Apply. if (GUI.Button(middleArea, ApplyLabel, styles.middle)) Apply(); // Auto Apply. var autoApply = AutoApply; if (autoApply && GUI.enabled) Apply(); GUI.enabled = enabled; if (autoApply != GUI.Toggle(rightArea, autoApply, AutoApplyLabel, styles.right)) AutoApply = !autoApply; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif