using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Sequences;
using UnityEngine.Timeline;
using UnityEngine.UIElements;
namespace UnityEditor.Sequences
{
[PackageHelpURL("sequence-assembly-window")]
partial class SequenceAssemblyWindow : BaseEditorWindow
{
SequenceAssemblyInspector m_CachedEditor;
Label m_SequenceNameLabel;
TextElement m_SequenceEmptyStateText;
VisualElement m_EmptyStateMessageContainer;
readonly string k_SequenceNotCreatedMessage = "Create and select a Sequence";
readonly string k_SequenceNotSelectedMessage = "Select a Sequence";
internal class Styles
{
public static readonly string k_AssemblyStyleSheetName = "SequenceAssemblyInspector";
public static readonly string k_AssemblyEmptyStateMessageContainer = "seq-empty-state-msg";
public static readonly string k_AssemblyEmptyStateText = "seq-empty-text-element";
}
///
/// Called by OnEnable sets the view
///
protected override void SetupView()
{
base.SetupView();
StyleSheetUtility.SetStyleSheets(rootVisualElement, Styles.k_AssemblyStyleSheetName);
titleContent = new GUIContent("Sequence Assembly",
IconUtility.LoadIcon("MasterSequence/Shot", IconUtility.IconType.UniqueToSkin));
m_SequenceNameLabel = new Label { bindingPath = "m_Name" };
m_SequenceEmptyStateText = new TextElement();
m_EmptyStateMessageContainer = new VisualElement();
m_EmptyStateMessageContainer.Add(m_SequenceEmptyStateText);
m_EmptyStateMessageContainer.AddToClassList(Styles.k_AssemblyEmptyStateMessageContainer);
m_SequenceEmptyStateText.AddToClassList(Styles.k_AssemblyEmptyStateText);
InitializeView();
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
SelectionUtility.playableDirectorChanged += ShowSelection;
ObjectChangeEvents.changesPublished += OnObjectChanged;
SequenceIndexer.validityChanged += OnSequenceInvalidated;
SequenceIndexer.sequencesRemoved += OnSequenceInvalidated; // Timeline asset removed on disk (not via API).
}
void OnDestroy()
{
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
SelectionUtility.playableDirectorChanged -= ShowSelection;
ObjectChangeEvents.changesPublished -= OnObjectChanged;
SequenceIndexer.validityChanged -= OnSequenceInvalidated;
SequenceIndexer.sequencesRemoved -= OnSequenceInvalidated;
ClearView();
}
void OnFocus()
{
if (m_CachedEditor != null)
m_CachedEditor.SelectPlayableDirector();
}
void InitializeView()
{
SetHeaderContent(m_SequenceNameLabel);
if (m_CachedEditor && m_CachedEditor.target != null) // Domain reload or going in PlayMode might trigger this user case.
{
if (m_CachedEditor.rootVisualElement == null)
m_CachedEditor.Initialize();
rootVisualElement.Bind(new SerializedObject((m_CachedEditor.target as PlayableDirector).playableAsset as TimelineAsset));
rootVisualElement.Add(m_CachedEditor.rootVisualElement);
}
else // The Editor doesn't exist or the target is null.
ShowSelection();
}
void ShowSelection()
{
// An existing view's target becomes null when the user deletes the sequence game object,
// in which case the view should show nothing.
if (m_CachedEditor != null && m_CachedEditor.target == null)
{
ClearView();
return;
}
var director = SelectionUtility.activePlayableDirector;
var isSelectionSequence = director != null &&
director.gameObject == Selection.activeGameObject &&
director.playableAsset != null &&
director.gameObject.GetComponent() != null;
// If the window is not showing any sequence and the selected playable director is not a sequence: refresh
// the empty state message.
if (m_CachedEditor == null && !isSelectionSequence)
{
SetEmptyState();
return;
}
// If the window is already showing the expected sequence, there's no need to change the view.
if (!isSelectionSequence || IsAlreadyShown(director))
return;
// The sequence to show was changed.
ClearView();
CreateView(director);
}
bool IsAlreadyShown(PlayableDirector target)
{
return (m_CachedEditor && m_CachedEditor.target == target);
}
void CreateView(PlayableDirector data)
{
m_EmptyStateMessageContainer.RemoveFromHierarchy();
if (!EditorApplication.isPlayingOrWillChangePlaymode)
m_CachedEditor = SequenceAssemblyInspector.CreateEditor(
data,
typeof(SequenceAssemblyInspector)) as SequenceAssemblyInspector;
else
m_CachedEditor = SequenceAssemblyInspector.CreateEditor(
data,
typeof(SequenceAssemblyPlayModeInspector)) as SequenceAssemblyPlayModeInspector;
m_CachedEditor.Initialize();
rootVisualElement.Add(m_CachedEditor.CreateInspectorGUI());
rootVisualElement.Bind(new SerializedObject(data.playableAsset as TimelineAsset));
}
void ClearView()
{
if (m_CachedEditor && m_CachedEditor.rootVisualElement != null)
{
if (!rootVisualElement.Contains(m_CachedEditor.rootVisualElement))
return;
rootVisualElement.Remove(m_CachedEditor.rootVisualElement);
rootVisualElement.Unbind();
m_SequenceNameLabel.text = "";
m_CachedEditor.Unload();
DestroyImmediate(m_CachedEditor);
}
SetEmptyState();
}
void OnPlayModeStateChanged(PlayModeStateChange stateChange)
{
// Check if the Sequence that has been shown before PlayMode is still there.
// It could be null if scenes are dynamically loaded for example.
var lastDirector = m_CachedEditor != null ? m_CachedEditor.target as PlayableDirector : null;
if (m_CachedEditor != null)
ClearView();
// TODO: remove check to `lastDirector.playableAsset` when we make the code detect
// the deletion of a Sequence.
// Temporary check to ensure the view we get is still on a valid TimelineAsset.
// Could be null if a user deletes it manually from the Project for example.
if (lastDirector != null && lastDirector.playableAsset != null)
CreateView(lastDirector);
}
void OnObjectChanged(ref ObjectChangeEventStream stream)
{
for (int i = 0; i < stream.length; ++i)
{
var eventType = stream.GetEventType(i);
switch (eventType)
{
case ObjectChangeKind.ChangeScene:
stream.GetChangeSceneEvent(i, out var sceneChangeData);
OnSceneChanged(sceneChangeData);
break;
case ObjectChangeKind.CreateGameObjectHierarchy:
stream.GetCreateGameObjectHierarchyEvent(i, out var createdGameObjectData);
OnCreationOfGameObjectHierarchy(createdGameObjectData);
break;
case ObjectChangeKind.DestroyGameObjectHierarchy:
stream.GetDestroyGameObjectHierarchyEvent(i, out var destroyGameObjectData);
OnDestroyGameObjectHierarchy(destroyGameObjectData);
break;
}
}
}
void OnSceneChanged(ChangeSceneEventArgs data)
{
if (m_CachedEditor != null)
{
// PlayableDirector got deleted. Possibly from deleting a MasterSequence/Sequence
// as it doesn't register the operation to undo but still mark the scene Dirty.
if (m_CachedEditor.target == null)
ClearView();
else if ((m_CachedEditor.target as PlayableDirector).gameObject.scene == data.scene)
m_CachedEditor.Refresh();
}
}
void OnCreationOfGameObjectHierarchy(CreateGameObjectHierarchyEventArgs data)
{
var createdGameObject = EditorUtility.InstanceIDToObject(data.instanceId) as GameObject;
if (createdGameObject == null)
return;
if (m_CachedEditor == null || m_CachedEditor.target == null)
return;
var directorGameObject = (m_CachedEditor.target as PlayableDirector).gameObject;
if (IsChildOf(createdGameObject, directorGameObject))
m_CachedEditor.Refresh();
}
void OnDestroyGameObjectHierarchy(DestroyGameObjectHierarchyEventArgs data)
{
var parentGameObject = EditorUtility.InstanceIDToObject(data.parentInstanceId) as GameObject;
// A GameObject has been destroyed outside of a Sequence.
if (parentGameObject == null)
return;
// No sequence is inspected, nothing to do.
if (m_CachedEditor == null)
return;
var director = m_CachedEditor.target as PlayableDirector;
if (director == null)
{
// The inspected Sequence has been deleted.
ClearView();
return;
}
// A GameObject within a Sequence tree has been deleted.
var directorGameObject = director.gameObject;
if (parentGameObject == directorGameObject || IsChildOf(parentGameObject, directorGameObject))
m_CachedEditor.Refresh();
}
void OnSequenceInvalidated()
{
if (m_CachedEditor == null || m_CachedEditor.target == null)
{
// There is no sequence currently inspected. Refresh the empty state message.
SetEmptyState();
return;
}
var target = m_CachedEditor.target as PlayableDirector;
var targetTimeline = target.playableAsset as TimelineAsset;
if (targetTimeline == null)
{
// Use cases:
// - Timeline asset is deleted on disk (not via API).
// - PlayableDirector binding is removed.
ClearView();
}
else
{
// Use case: sequence editorial clip binding is removed.
var sequence = SequenceIndexer.instance.GetSequence(targetTimeline);
if (!sequence.isValid)
ClearView();
}
}
bool IsChildOf(GameObject child, GameObject parent)
{
for (var parentPointer = child.transform.parent; parentPointer != null; parentPointer = parentPointer.parent)
{
if (parentPointer.gameObject == parent)
return true;
}
return false;
}
void SetEmptyState()
{
if (!rootVisualElement.Contains(m_EmptyStateMessageContainer))
{
rootVisualElement.Add(m_EmptyStateMessageContainer);
rootVisualElement.Unbind();
m_SequenceNameLabel.text = "";
}
m_SequenceEmptyStateText.text = SequenceIndexer.instance.isEmpty ? k_SequenceNotCreatedMessage : k_SequenceNotSelectedMessage;
}
}
}