SignalEmitterInspector.cs 13 KB
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
using UnityObject = UnityEngine.Object;

namespace UnityEditor.Timeline.Signals
{
    [CustomEditor(typeof(SignalEmitter), true)]
    [CanEditMultipleObjects]
    class SignalEmitterInspector : MarkerInspector, ISignalAssetProvider
    {
        SerializedProperty m_RetroactiveProperty;
        SerializedProperty m_EmitOnceProperty;

        SignalEmitter m_Signal;
        GameObject m_BoundGameObject;
        PlayableDirector m_AssociatedDirector;
        bool m_TargetsHaveTheSameBinding;

        readonly Dictionary<Component, Editor> m_Editors = new Dictionary<Component, Editor>();
        readonly Dictionary<Component, bool> m_Foldouts = new Dictionary<Component, bool>();
        List<Component> m_Receivers = new List<Component>();

        static GUIStyle s_FoldoutStyle;
        internal static GUIStyle foldoutStyle
        {
            get
            {
                if (s_FoldoutStyle == null)
                {
                    s_FoldoutStyle = new GUIStyle(EditorStyles.foldout) {fontStyle = FontStyle.Bold};
                }

                return s_FoldoutStyle;
            }
        }

        public SignalAsset signalAsset
        {
            get
            {
                var emitter = target as SignalEmitter;
                return signalAssetSameValue ? emitter.asset : null;
            }
            set
            {
                AssignSignalAsset(value);
            }
        }

        bool signalAssetSameValue
        {
            get
            {
                var emitters = targets.Cast<SignalEmitter>().ToList();
                return emitters.Select(x => x.asset).Distinct().Count() == 1;
            }
        }

        void OnEnable()
        {
            Undo.undoRedoPerformed += OnUndoRedo; // subscribe to the event
            m_Signal = target as SignalEmitter;
            m_RetroactiveProperty = serializedObject.FindProperty("m_Retroactive");
            m_EmitOnceProperty = serializedObject.FindProperty("m_EmitOnce");
            // In a vast majority of the cases, when this becomes enabled,
            // the timeline window will be focused on the correct timeline
            // in which case TimelineEditor.inspectedDirector is safe to use
            m_AssociatedDirector = TimelineEditor.inspectedDirector;
            UpdateState();
        }

        internal override bool IsEnabled()
        {
            return TimelineUtility.IsCurrentSequenceValid() && !IsCurrentSequenceReadOnly() && base.IsEnabled();
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();

            using (var changeScope = new EditorGUI.ChangeCheckScope())
            {
                var property = serializedObject.GetIterator();
                var expanded = true;
                while (property.NextVisible(expanded))
                {
                    expanded = false;
                    if (SkipField(property.propertyPath))
                        continue;
                    EditorGUILayout.PropertyField(property, true);
                }

                DrawSignalFlags();
                UpdateState();
                DrawNameSelectorAndSignalList();

                if (changeScope.changed)
                {
                    serializedObject.ApplyModifiedProperties();
                    TimelineEditor.Refresh(RefreshReason.ContentsModified | RefreshReason.WindowNeedsRedraw);
                }
            }
        }

        internal override void OnHeaderIconGUI(Rect iconRect)
        {
            using (new EditorGUI.DisabledScope(!TimelineUtility.IsCurrentSequenceValid() || IsCurrentSequenceReadOnly()))
            {
                GUI.Label(iconRect, Styles.SignalEmitterIcon);
            }
        }

        internal override Rect DrawHeaderHelpAndSettingsGUI(Rect r)
        {
            using (new EditorGUI.DisabledScope(!TimelineUtility.IsCurrentSequenceValid() || IsCurrentSequenceReadOnly()))
            {
                var helpSize = EditorStyles.iconButton.CalcSize(EditorGUI.GUIContents.helpIcon);
                const int kTopMargin = 5;
                return EditorGUIUtility.DrawEditorHeaderItems(new Rect(r.xMax - helpSize.x, r.y + kTopMargin, helpSize.x, helpSize.y), targets);
            }
        }

        IEnumerable<SignalAsset> ISignalAssetProvider.AvailableSignalAssets()
        {
            return SignalManager.assets;
        }

        void ISignalAssetProvider.CreateNewSignalAsset(string path)
        {
            var newSignalAsset = SignalManager.CreateSignalAssetInstance(path);
            AssignSignalAsset(newSignalAsset);
            var receivers = m_Receivers.OfType<SignalReceiver>().ToList();
            if (signalAsset != null && receivers.Count == 1 && !receivers.Any(r => r.IsSignalAssetHandled(newSignalAsset))) // Only when one receiver is present
            {
                receivers[0].AddNewReaction(newSignalAsset); // Add reaction on the first receiver from the list
                ApplyChangesAndRefreshReceivers();
            }
        }

        void UpdateState()
        {
            m_BoundGameObject = GetBoundGameObject(m_Signal.parent, m_AssociatedDirector);
            m_Receivers = m_BoundGameObject == null || m_BoundGameObject.Equals(null)
                ? new List<Component>()
                : m_BoundGameObject.GetComponents<Component>().Where(t => t is INotificationReceiver).ToList();

            m_TargetsHaveTheSameBinding = targets.Cast<SignalEmitter>()
                .Select(x => GetBoundGameObject(x.parent, m_AssociatedDirector))
                .Distinct().Count() == 1;
        }

        Editor GetOrCreateReceiverEditor(Component c)
        {
            Editor ret;
            if (m_Editors.TryGetValue(c, out ret))
            {
                return ret;
            }

            ret = CreateEditorWithContext(new Object[] {c}, target);
            m_Editors[c] = ret;
            if (!m_Foldouts.ContainsKey(c))
            {
                m_Foldouts[c] = true;
            }

            return ret;
        }

        void OnDisable()
        {
            Undo.undoRedoPerformed -= OnUndoRedo;
        }

        void OnDestroy()
        {
            foreach (var editor in m_Editors)
            {
                DestroyImmediate(editor.Value);
            }
            m_Editors.Clear();
        }

        void OnUndoRedo()
        {
            ApplyChangesAndRefreshReceivers();
        }

        void ApplyChangesAndRefreshReceivers()
        {
            foreach (var receiverInspector in m_Editors.Values.OfType<SignalReceiverInspector>())
            {
                receiverInspector.SetAssetContext(signalAsset);
            }
        }

        void DrawNameSelectorAndSignalList()
        {
            using (var change = new EditorGUI.ChangeCheckScope())
            {
                DrawSignal();
                DrawReceivers();

                if (change.changed)
                {
                    ApplyChangesAndRefreshReceivers();
                }
            }
        }

        void DrawReceivers()
        {
            if (!m_TargetsHaveTheSameBinding)
            {
                EditorGUILayout.HelpBox(Styles.MultiEditNotSupportedOnDifferentBindings, MessageType.None);
                return;
            }

            if (targets.OfType<SignalEmitter>().Select(x => x.asset).Distinct().Count() > 1)
            {
                EditorGUILayout.HelpBox(Styles.MultiEditNotSupportedOnDifferentSignals, MessageType.None);
                return;
            }

            //do not display the receiver if the current timeline is not the same as the emitter's timeline
            //can happen if the inspector is locked
            if (m_Signal.parent != null && m_Signal.parent.timelineAsset != TimelineEditor.inspectedAsset)
                return;

            if (m_BoundGameObject != null)
            {
                if (!m_Receivers.Any(x => x is SignalReceiver))
                {
                    EditorGUILayout.Separator();
                    var message = string.Format(Styles.NoSignalReceiverComponent, m_BoundGameObject.name);
                    SignalUtility.DrawCenteredMessage(message);
                    if (SignalUtility.DrawCenteredButton(Styles.AddSignalReceiverComponent))
                        AddReceiverComponent();
                }

                foreach (var receiver in m_Receivers)
                {
                    var editor = GetOrCreateReceiverEditor(receiver);
                    if (DrawReceiverHeader(receiver))
                    {
                        editor.OnInspectorGUI();
                    }
                }
            }
            else if (m_AssociatedDirector != null) //not in asset mode
            {
                EditorGUILayout.HelpBox(Styles.NoBoundGO, MessageType.None);
            }
        }

        void DrawSignalFlags()
        {
            EditorGUILayout.PropertyField(m_RetroactiveProperty, Styles.RetroactiveLabel);
            EditorGUILayout.PropertyField(m_EmitOnceProperty, Styles.EmitOnceLabel);
        }

        void DrawSignal()
        {
            //should show button to create new signal if there are no signals asset in the project
            if (!SignalManager.assets.Any())
            {
                using (new EditorGUI.DisabledScope(true))
                {
                    DrawNameSelector();
                }

                EditorGUILayout.Separator();
                SignalUtility.DrawCenteredMessage(Styles.ProjectHasNoSignalAsset);
                if (SignalUtility.DrawCenteredButton(Styles.CreateNewSignal))
                    CreateNewSignalAsset(SignalUtility.GetNewSignalPath());
                EditorGUILayout.Separator();
            }
            else
            {
                DrawNameSelector();
            }
        }

        internal void CreateNewSignalAsset(string path)
        {
            if (!string.IsNullOrEmpty(path))
                ((ISignalAssetProvider)this).CreateNewSignalAsset(path);
            GUIUtility.ExitGUI();
        }

        void AssignSignalAsset(SignalAsset newAsset)
        {
            foreach (var o in targets)
            {
                var signalEmitter = (SignalEmitter)o;
                TimelineUndo.PushUndo(signalEmitter, Styles.UndoCreateSignalAsset);
                signalEmitter.asset = newAsset;
            }
        }

        void DrawNameSelector()
        {
            SignalUtility.DrawSignalNames(this, EditorGUILayout.GetControlRect(), Styles.EmitSignalLabel, !signalAssetSameValue);
        }

        bool DrawReceiverHeader(Component receiver)
        {
            EditorGUILayout.Space();
            var lineRect = GUILayoutUtility.GetRect(10, 4, EditorStyles.inspectorTitlebar);
            DrawSplitLine(lineRect.y);

            var style = EditorGUIUtility.TrTextContentWithIcon(
                ObjectNames.NicifyVariableName(receiver.GetType().Name),
                AssetPreview.GetMiniThumbnail(receiver));

            m_Foldouts[receiver] =
                EditorGUILayout.Foldout(m_Foldouts[receiver], style, true, foldoutStyle);
            if (m_Foldouts[receiver])
            {
                DrawReceiverObjectField();
            }

            return m_Foldouts[receiver];
        }

        void DrawReceiverObjectField()
        {
            EditorGUI.BeginDisabledGroup(true);
            EditorGUILayout.ObjectField(Styles.ObjectLabel, m_BoundGameObject, typeof(GameObject), false);
            EditorGUI.EndDisabledGroup();
        }

        void AddReceiverComponent()
        {
            var receiver = Undo.AddComponent<SignalReceiver>(m_BoundGameObject);
            receiver.AddNewReaction(signalAsset);
        }

        static bool SkipField(string fieldName)
        {
            return fieldName == "m_Script" || fieldName == "m_Asset" || fieldName == "m_Retroactive" || fieldName == "m_EmitOnce";
        }

        static void DrawSplitLine(float y)
        {
            if (Event.current.type != EventType.Repaint) return;

            var width = EditorGUIUtility.currentViewWidth;
            var position = new Rect(0, y, width + 1, 1);

            if (EditorStyles.inspectorTitlebar != null)
                EditorStyles.inspectorTitlebar.Draw(position, false, false, false, false);
        }

        static GameObject GetBoundGameObject(TrackAsset track, PlayableDirector associatedDirector)
        {
            if (associatedDirector == null || track == null) //if in asset mode, no bound object for you
                return null;

            var boundObj = TimelineUtility.GetSceneGameObject(associatedDirector, track);

            //if the signal is on the timeline marker track and user did not set a binding, assume it's bound to PlayableDirector
            if (boundObj == null && track.timelineAsset.markerTrack == track)
                boundObj = associatedDirector.gameObject;

            return boundObj;
        }

        static bool IsCurrentSequenceReadOnly()
        {
            return TimelineWindow.instance.state.editSequence.isReadOnly;
        }
    }
}