| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 | // Animancer // Copyright 2018-2024 Kybernetik //#if UNITY_EDITORusing System;using System.Collections.Generic;using UnityEditor;using UnityEngine;using Object = UnityEngine.Object;namespace Animancer.Editor{    /// <summary>[Editor-Only]    /// A window for managing a copy of some serialized data and applying or reverting it.    /// </summary>    /// <remarks>    /// This system assumes the implementation of <see cref="IEquatable{T}"/>    /// compares the values of all fields in <typeparamref name="TData"/>.    /// </remarks>    /// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializedDataEditorWindow_2    public abstract class SerializedDataEditorWindow<TObject, TData> : EditorWindow        where TObject : Object        where TData : class, ICopyable<TData>, IEquatable<TData>, new()    {        /************************************************************************************************************************/        [SerializeField]        private TObject _SourceObject;        /// <summary>The object which contains the data this class manages.</summary>        /// <remarks><see cref="SetAndCaptureSource"/> should generally be used instead of setting this property directly.</remarks>        public virtual TObject SourceObject        {            get => _SourceObject;            protected set => _SourceObject = value;        }        /************************************************************************************************************************/        /// <summary>The <see cref="Data"/> field of the <see cref="SourceObject"/>.</summary>        public abstract TData SourceData { get; set; }        /************************************************************************************************************************/        [SerializeField]        private TData _Data;        /// <summary>A copy of the <see cref="SourceData"/> being managed by this window.</summary>        public ref TData Data            => ref _Data;        /************************************************************************************************************************/        /// <summary>Is the <see cref="Data"/> managed by this window different to the <see cref="SourceData"/>.</summary>        public bool HasDataChanged        {            get            {                try                {                    return _Data != null && !_Data.Equals(SourceData);                }                catch (Exception exception)                {                    Debug.LogException(exception);                    return false;                }            }        }        /************************************************************************************************************************/        /// <summary>Initializes this window.</summary>        protected virtual void OnEnable()        {            EditorApplication.playModeStateChanged += OnPlayModeStateChanged;            EditorApplication.wantsToQuit += OnTryCloseEditor;            Undo.undoRedoPerformed += Repaint;        }        /// <summary>Cleans up this window.</summary>        protected virtual void OnDisable()        {            EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;            EditorApplication.wantsToQuit -= OnTryCloseEditor;            Undo.undoRedoPerformed -= Repaint;        }        /************************************************************************************************************************/        /// <summary>        /// Prompts the user to <see cref="Apply"/> or <see cref="Revert"/>        /// if there are changes in the <see cref="Data"/> when this window is closed.        /// </summary>        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();            }        }        /************************************************************************************************************************/        /// <summary>Called before closing the Unity Editor to confirm that un-saved data is applied.</summary>        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;            }        }        /************************************************************************************************************************/        /// <summary>        /// Sets the <see cref="SourceObject"/> and captures the <see cref="Data"/>        /// as a copy of its <see cref="SourceData"/>.        /// </summary>        protected void SetAndCaptureSource(TObject sourceObject)        {            _SourceObject = sourceObject;            CaptureData();            Repaint();        }        /************************************************************************************************************************/        /// <summary>        /// Override this to return <c>true</c> if the <see cref="SourceObject"/> could be part of a prefab        /// to ensure that modifications are serialized properly.        /// </summary>        public virtual bool SourceObjectMightBePrefab            => false;        /************************************************************************************************************************/        /// <summary>Saves the edited <see cref="Data"/> into the <see cref="SourceObject"/>.</summary>        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<Object>();                    GatherObjectReferences(sourceObject, objects);                    foreach (var obj in objects)                        if (!EditorUtility.IsPersistent(obj))                            AssetDatabase.AddObjectToAsset(obj, SourceObject);                    SetPool.Release(objects);                }            }            Repaint();            AssetDatabase.SaveAssets();        }        /************************************************************************************************************************/        /// <summary>Gathers all objects referenced by the `root`.</summary>        public static void GatherObjectReferences(Object root, HashSet<Object> 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);                }            }        }        /************************************************************************************************************************/        /// <summary>Restores the <see cref="Data"/> to the original values from the <see cref="SourceData"/>.</summary>        public virtual void Revert()        {            RecordUndo();            CaptureData();        }        /************************************************************************************************************************/        /// <summary>Stores a copy of the <see cref="SourceData"/> in the <see cref="Data"/>.</summary>        protected virtual void CaptureData()        {            _Data = SourceData?.CopyableClone() ?? new();            AnimancerReflection.TryInvoke(_Data, "OnValidate");        }        /************************************************************************************************************************/        /// <summary>Records the current state of this window so it can be undone later.</summary>        public TData RecordUndo()            => RecordUndo(titleContent.text);        /// <summary>Records the current state of this window so it can be undone later.</summary>        public virtual TData RecordUndo(string name)        {            Undo.RecordObject(this, name);            Repaint();            return _Data;        }        /************************************************************************************************************************/        /// <summary>        /// Opens a new <typeparamref name="TWindow"/> for the `sourceObject`        /// or gives focus to an existing window that was already displaying it.        /// </summary>        public static TWindow Open<TWindow>(            TObject sourceObject,            bool onlyOneWindow = false,            params Type[] desiredDockNextTo)            where TWindow : SerializedDataEditorWindow<TObject, TData>        {            if (!onlyOneWindow)            {                foreach (var window in Resources.FindObjectsOfTypeAll<TWindow>())                {                    if (window.SourceObject == sourceObject)                    {                        window.Show();                        window.SetAndCaptureSource(sourceObject);                        window.Focus();                        return window;                    }                }            }            var newWindow = onlyOneWindow                ? GetWindow<TWindow>(desiredDockNextTo ?? Type.EmptyTypes)                : CreateInstance<TWindow>();            newWindow.Show();            newWindow.SetAndCaptureSource(sourceObject);            return newWindow;        }        /************************************************************************************************************************/        #region Auto Apply        /************************************************************************************************************************/        /// <summary>The <see cref="EditorPrefs"/> key for <see cref="AutoApply"/>.</summary>        protected virtual string AutoApplyPref            => $"{titleContent.text}.{nameof(AutoApply)}";        /************************************************************************************************************************/        private bool _HasLoadedAutoApply;        private bool _AutoApply;        private bool _EnabledAutoApplyInPlayMode;        /// <summary>Is the "Auto Apply" toggle currently enabled?</summary>        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);            }        }        /************************************************************************************************************************/        /// <summary>Handles entering and exiting Play Mode.</summary>        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.");        /************************************************************************************************************************/        /// <summary>        /// Calculates the pixel width required for        /// <see cref="DoApplyRevertGUI(Rect, Rect, Rect, ButtonGroupStyles)"/>.        /// </summary>        public float CalculateApplyRevertWidth(ButtonGroupStyles styles = default)        {            styles.CopyMissingStyles(ButtonGroupStyles.Button);            return                styles.left.CalculateWidth(RevertLabel) +                styles.middle.CalculateWidth(ApplyLabel) +                styles.right.CalculateWidth(AutoApplyLabel);        }        /************************************************************************************************************************/        /// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>        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();        }        /************************************************************************************************************************/        /// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>        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);        }        /************************************************************************************************************************/        /// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>        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
 |