378 lines
13 KiB
C#
378 lines
13 KiB
C#
|
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;
|
||
|
}
|
||
|
}
|
||
|
}
|