542 lines
18 KiB
C#
542 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEditor.Callbacks;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using UnityEngine.Playables;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.Timeline;
|
|
|
|
namespace UnityEditor.Timeline
|
|
{
|
|
[EditorWindowTitle(title = "Timeline", useTypeNameAsIconName = true)]
|
|
partial class TimelineWindow : EditorWindow, IHasCustomMenu
|
|
{
|
|
[Serializable]
|
|
public class TimelineWindowPreferences
|
|
{
|
|
public bool frameSnap = true;
|
|
public bool edgeSnaps = true;
|
|
public bool muteAudioScrub = true;
|
|
public bool playRangeLoopMode = true;
|
|
public PlaybackScrollMode autoScrollMode;
|
|
public EditMode.EditType editType = EditMode.EditType.Mix;
|
|
public TimeReferenceMode timeReferenceMode = TimeReferenceMode.Local;
|
|
}
|
|
|
|
[SerializeField] TimelineWindowPreferences m_Preferences = new TimelineWindowPreferences();
|
|
public TimelineWindowPreferences preferences { get { return m_Preferences; } }
|
|
|
|
[SerializeField]
|
|
EditorGUIUtility.EditorLockTracker m_LockTracker = new EditorGUIUtility.EditorLockTracker();
|
|
|
|
readonly PreviewResizer m_PreviewResizer = new PreviewResizer();
|
|
bool m_LastFrameHadSequence;
|
|
bool m_ForceRefreshLastSelection;
|
|
int m_CurrentSceneHashCode = -1;
|
|
|
|
[NonSerialized]
|
|
bool m_HasBeenInitialized;
|
|
|
|
[SerializeField]
|
|
SequenceHierarchy m_SequenceHierarchy;
|
|
static SequenceHierarchy s_LastHierarchy;
|
|
|
|
public static TimelineWindow instance { get; private set; }
|
|
public Rect clientArea { get; set; }
|
|
public bool isDragging { get; set; }
|
|
public static DirectorStyles styles { get { return DirectorStyles.Instance; } }
|
|
public List<TimelineTrackBaseGUI> allTracks
|
|
{
|
|
get
|
|
{
|
|
return treeView != null ? treeView.allTrackGuis : new List<TimelineTrackBaseGUI>();
|
|
}
|
|
}
|
|
|
|
public WindowState state { get; private set; }
|
|
|
|
public bool locked
|
|
{
|
|
get
|
|
{
|
|
// we can never be in a locked state if there is no timeline asset
|
|
if (state.editSequence.asset == null)
|
|
return false;
|
|
|
|
return m_LockTracker.isLocked;
|
|
}
|
|
set { m_LockTracker.isLocked = value; }
|
|
}
|
|
|
|
public bool hierarchyChangedThisFrame { get; private set; }
|
|
|
|
public TimelineWindow()
|
|
{
|
|
InitializeManipulators();
|
|
m_LockTracker.lockStateChanged.AddPersistentListener(OnLockStateChanged, UnityEventCallState.EditorAndRuntime);
|
|
}
|
|
|
|
void OnLockStateChanged(bool locked)
|
|
{
|
|
// Make sure that upon unlocking, any selection change is updated
|
|
// Case 1123119 -- only force rebuild if not recording
|
|
if (!locked)
|
|
RefreshSelection(state != null && !state.recording);
|
|
}
|
|
|
|
void OnEnable()
|
|
{
|
|
if (m_SequencePath == null)
|
|
m_SequencePath = new SequencePath();
|
|
|
|
if (m_SequenceHierarchy == null)
|
|
{
|
|
// The sequence hierarchy will become null if maximize on play is used for in/out of playmode
|
|
// a static var will hang on to the reference
|
|
if (s_LastHierarchy != null)
|
|
m_SequenceHierarchy = s_LastHierarchy;
|
|
else
|
|
m_SequenceHierarchy = SequenceHierarchy.CreateInstance();
|
|
|
|
state = null;
|
|
}
|
|
s_LastHierarchy = m_SequenceHierarchy;
|
|
|
|
titleContent = GetLocalizedTitleContent();
|
|
|
|
m_PreviewResizer.Init("TimelineWindow");
|
|
|
|
// Unmaximize fix : when unmaximizing, a new window is enabled and disabled. Prevent it from overriding the instance pointer.
|
|
if (instance == null)
|
|
instance = this;
|
|
|
|
AnimationClipCurveCache.Instance.OnEnable();
|
|
TrackAsset.OnClipPlayableCreate += m_PlayableLookup.UpdatePlayableLookup;
|
|
TrackAsset.OnTrackAnimationPlayableCreate += m_PlayableLookup.UpdatePlayableLookup;
|
|
|
|
if (state == null)
|
|
{
|
|
state = new WindowState(this, s_LastHierarchy);
|
|
Initialize();
|
|
RefreshSelection(true);
|
|
m_ForceRefreshLastSelection = true;
|
|
}
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
if (instance == this)
|
|
instance = null;
|
|
|
|
if (state != null)
|
|
state.Reset();
|
|
|
|
if (instance == null)
|
|
SelectionManager.RemoveTimelineSelection();
|
|
|
|
AnimationClipCurveCache.Instance.OnDisable();
|
|
TrackAsset.OnClipPlayableCreate -= m_PlayableLookup.UpdatePlayableLookup;
|
|
TrackAsset.OnTrackAnimationPlayableCreate -= m_PlayableLookup.UpdatePlayableLookup;
|
|
TimelineWindowViewPrefs.SaveAll();
|
|
TimelineWindowViewPrefs.UnloadAllViewModels();
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
if (state != null)
|
|
{
|
|
state.OnDestroy();
|
|
}
|
|
m_HasBeenInitialized = false;
|
|
RemoveEditorCallbacks();
|
|
AnimationClipCurveCache.Instance.Clear();
|
|
TimelineAnimationUtilities.UnlinkAnimationWindow();
|
|
}
|
|
|
|
void OnLostFocus()
|
|
{
|
|
isDragging = false;
|
|
|
|
if (state != null)
|
|
state.captured.Clear();
|
|
|
|
Repaint();
|
|
}
|
|
|
|
void OnFocus()
|
|
{
|
|
if (state == null) return;
|
|
|
|
if (lastSelectedGO != Selection.activeObject)
|
|
{
|
|
// selection may have changed while Timeline Editor was looking away
|
|
RefreshSelection(false);
|
|
|
|
// Inline curves may have become out of sync
|
|
RefreshInlineCurves();
|
|
}
|
|
}
|
|
|
|
void OnHierarchyChange()
|
|
{
|
|
hierarchyChangedThisFrame = true;
|
|
Repaint();
|
|
}
|
|
|
|
void OnStateChange()
|
|
{
|
|
state.UpdateRecordingState();
|
|
if (treeView != null && state.editSequence.asset != null)
|
|
treeView.Reload();
|
|
if (m_MarkerHeaderGUI != null)
|
|
m_MarkerHeaderGUI.Rebuild();
|
|
}
|
|
|
|
void OnGUI()
|
|
{
|
|
InitializeGUIIfRequired();
|
|
UpdateGUIConstants();
|
|
UpdateViewStateHash();
|
|
|
|
EditMode.HandleModeClutch(); // TODO We Want that here?
|
|
|
|
DetectStylesChange();
|
|
DetectActiveSceneChanges();
|
|
DetectStateChanges();
|
|
|
|
state.ProcessStartFramePendingUpdates();
|
|
|
|
var clipRect = new Rect(0.0f, 0.0f, position.width, position.height);
|
|
clipRect.xMin += state.sequencerHeaderWidth;
|
|
|
|
using (new GUIViewportScope(clipRect))
|
|
state.InvokeWindowOnGuiStarted(Event.current);
|
|
|
|
if (Event.current.type == EventType.MouseDrag && state != null && state.mouseDragLag > 0.0f)
|
|
{
|
|
state.mouseDragLag -= Time.deltaTime;
|
|
return;
|
|
}
|
|
|
|
if (PerformUndo())
|
|
return;
|
|
|
|
if (EditorApplication.isPlaying)
|
|
{
|
|
if (state != null)
|
|
{
|
|
if (state.recording)
|
|
state.recording = false;
|
|
}
|
|
Repaint();
|
|
}
|
|
|
|
clientArea = position;
|
|
|
|
PlaybackScroller.AutoScroll(state);
|
|
DoLayout();
|
|
|
|
// overlays
|
|
if (state.captured.Count > 0)
|
|
{
|
|
using (new GUIViewportScope(clipRect))
|
|
{
|
|
foreach (var o in state.captured)
|
|
{
|
|
o.Overlay(Event.current, state);
|
|
}
|
|
Repaint();
|
|
}
|
|
}
|
|
|
|
if (state.showQuadTree)
|
|
state.spacePartitioner.DebugDraw();
|
|
|
|
// attempt another rebuild -- this will avoid 1 frame flashes
|
|
if (Event.current.type == EventType.Repaint)
|
|
{
|
|
RebuildGraphIfNecessary();
|
|
state.ProcessEndFramePendingUpdates();
|
|
}
|
|
|
|
using (new GUIViewportScope(clipRect))
|
|
{
|
|
if (Event.current.type == EventType.Repaint)
|
|
EditMode.inputHandler.OnGUI(state, Event.current);
|
|
}
|
|
|
|
if (Event.current.type == EventType.Repaint)
|
|
hierarchyChangedThisFrame = false;
|
|
}
|
|
|
|
static void DetectStylesChange()
|
|
{
|
|
DirectorStyles.ReloadStylesIfNeeded();
|
|
}
|
|
|
|
void DetectActiveSceneChanges()
|
|
{
|
|
if (m_CurrentSceneHashCode == -1)
|
|
{
|
|
m_CurrentSceneHashCode = SceneManager.GetActiveScene().GetHashCode();
|
|
}
|
|
|
|
if (m_CurrentSceneHashCode != SceneManager.GetActiveScene().GetHashCode())
|
|
{
|
|
bool isSceneStillLoaded = false;
|
|
for (int a = 0; a < SceneManager.sceneCount; a++)
|
|
{
|
|
var scene = SceneManager.GetSceneAt(a);
|
|
if (scene.GetHashCode() == m_CurrentSceneHashCode && scene.isLoaded)
|
|
{
|
|
isSceneStillLoaded = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isSceneStillLoaded)
|
|
{
|
|
if (!locked)
|
|
ClearCurrentTimeline();
|
|
m_CurrentSceneHashCode = SceneManager.GetActiveScene().GetHashCode();
|
|
}
|
|
}
|
|
}
|
|
|
|
void DetectStateChanges()
|
|
{
|
|
if (state != null)
|
|
{
|
|
state.editSequence.ResetIsReadOnly(); //Force reset readonly for asset flag for each frame.
|
|
// detect if the sequence was removed under our feet
|
|
if (m_LastFrameHadSequence && state.editSequence.asset == null)
|
|
{
|
|
ClearCurrentTimeline();
|
|
}
|
|
m_LastFrameHadSequence = state.editSequence.asset != null;
|
|
|
|
// the currentDirector can get set to null by a deletion or scene unloading so polling is required
|
|
if (state.editSequence.director == null)
|
|
{
|
|
state.recording = false;
|
|
state.previewMode = false;
|
|
|
|
//Case 1201405 : Check if the lock state is valid with the lock tracker state
|
|
if (locked != m_LockTracker.isLocked)
|
|
m_LockTracker.isLocked = locked;
|
|
|
|
if (!locked && m_LastFrameHadSequence)
|
|
{
|
|
// the user may be adding a new PlayableDirector to a selected GameObject, make sure the timeline editor is shows the proper director if none is already showing
|
|
var selectedGameObject = Selection.activeObject != null ? Selection.activeObject as GameObject : null;
|
|
var selectedDirector = selectedGameObject != null ? selectedGameObject.GetComponent<PlayableDirector>() : null;
|
|
if (selectedDirector != null)
|
|
{
|
|
SetCurrentTimeline(selectedDirector);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// the user may have changed the timeline associated with the current director
|
|
if (state.editSequence.asset != state.editSequence.director.playableAsset)
|
|
{
|
|
if (!locked)
|
|
{
|
|
SetCurrentTimeline(state.editSequence.director);
|
|
}
|
|
else
|
|
{
|
|
// Keep locked on the current timeline but set the current director to null since it's not the timeline owner anymore
|
|
SetCurrentTimeline(state.editSequence.asset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Initialize()
|
|
{
|
|
if (!m_HasBeenInitialized)
|
|
{
|
|
InitializeStateChange();
|
|
InitializeEditorCallbacks();
|
|
m_HasBeenInitialized = true;
|
|
}
|
|
}
|
|
|
|
void RefreshLastSelectionIfRequired()
|
|
{
|
|
// case 1088918 - workaround for the instanceID to object cache being update during Awake.
|
|
// This corrects any playableDirector ptrs with the correct cached version
|
|
// This can happen when going from edit to playmode
|
|
if (m_ForceRefreshLastSelection)
|
|
{
|
|
m_ForceRefreshLastSelection = false;
|
|
RestoreLastSelection(true);
|
|
}
|
|
}
|
|
|
|
void InitializeGUIIfRequired()
|
|
{
|
|
RefreshLastSelectionIfRequired();
|
|
InitializeTimeArea();
|
|
if (treeView == null && state.editSequence.asset != null)
|
|
{
|
|
treeView = new TimelineTreeViewGUI(this, state.editSequence.asset, position);
|
|
}
|
|
}
|
|
|
|
void UpdateGUIConstants()
|
|
{
|
|
m_HorizontalScrollBarSize =
|
|
GUI.skin.horizontalScrollbar.fixedHeight + GUI.skin.horizontalScrollbar.margin.top;
|
|
m_VerticalScrollBarSize = (treeView != null && treeView.showingVerticalScrollBar)
|
|
? GUI.skin.verticalScrollbar.fixedWidth + GUI.skin.verticalScrollbar.margin.left
|
|
: 0;
|
|
}
|
|
|
|
void UpdateViewStateHash()
|
|
{
|
|
if (Event.current.type == EventType.Layout)
|
|
state.UpdateViewStateHash();
|
|
}
|
|
|
|
static bool PerformUndo()
|
|
{
|
|
if (!Event.current.isKey)
|
|
return false;
|
|
|
|
if (Event.current.keyCode != KeyCode.Z)
|
|
return false;
|
|
|
|
if (!EditorGUI.actionKey)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public void RebuildGraphIfNecessary(bool evaluate = true)
|
|
{
|
|
if (state == null || state.editSequence.director == null || state.editSequence.asset == null)
|
|
return;
|
|
|
|
if (state.rebuildGraph)
|
|
{
|
|
// rebuilding the graph resets the time
|
|
double time = state.editSequence.time;
|
|
|
|
var wasPlaying = false;
|
|
|
|
// disable preview mode,
|
|
if (!EditorApplication.isPlaying)
|
|
{
|
|
wasPlaying = state.playing;
|
|
|
|
state.previewMode = false;
|
|
state.GatherProperties(state.masterSequence.director);
|
|
}
|
|
state.RebuildPlayableGraph();
|
|
state.editSequence.time = time;
|
|
|
|
if (wasPlaying)
|
|
state.Play();
|
|
|
|
if (evaluate)
|
|
{
|
|
// put the scene back in the correct state
|
|
state.EvaluateImmediate();
|
|
|
|
// this is necessary to see accurate results when inspector refreshes
|
|
// case 1154802 - this will property re-force time on the director, so
|
|
// the play head won't snap back to the timeline duration on rebuilds
|
|
if (!state.playing)
|
|
state.Evaluate();
|
|
}
|
|
Repaint();
|
|
}
|
|
|
|
state.rebuildGraph = false;
|
|
}
|
|
|
|
// for tests
|
|
public new void RepaintImmediately()
|
|
{
|
|
base.RepaintImmediately();
|
|
}
|
|
|
|
internal static bool IsEditingTimelineAsset(TimelineAsset timelineAsset)
|
|
{
|
|
return instance != null && instance.state != null && instance.state.editSequence.asset == timelineAsset;
|
|
}
|
|
|
|
internal static void RepaintIfEditingTimelineAsset(TimelineAsset timelineAsset)
|
|
{
|
|
if (IsEditingTimelineAsset(timelineAsset))
|
|
instance.Repaint();
|
|
}
|
|
|
|
internal class DoCreateTimeline : ProjectWindowCallback.EndNameEditAction
|
|
{
|
|
public override void Action(int instanceId, string pathName, string resourceFile)
|
|
{
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
AssetDatabase.CreateAsset(timeline, pathName);
|
|
ProjectWindowUtil.ShowCreatedAsset(timeline);
|
|
}
|
|
}
|
|
|
|
[MenuItem("Assets/Create/Timeline", false, 450)]
|
|
public static void CreateNewTimeline()
|
|
{
|
|
var icon = EditorGUIUtility.IconContent("TimelineAsset Icon").image as Texture2D;
|
|
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, ScriptableObject.CreateInstance<DoCreateTimeline>(), "New Timeline.playable", icon, null);
|
|
}
|
|
|
|
[MenuItem("Window/Sequencing/Timeline", false, 1)]
|
|
public static void ShowWindow()
|
|
{
|
|
GetWindow<TimelineWindow>(typeof(SceneView));
|
|
instance.Focus();
|
|
}
|
|
|
|
[OnOpenAsset(1)]
|
|
public static bool OnDoubleClick(int instanceID, int line)
|
|
{
|
|
var assetDoubleClicked = EditorUtility.InstanceIDToObject(instanceID) as TimelineAsset;
|
|
if (assetDoubleClicked == null)
|
|
return false;
|
|
|
|
ShowWindow();
|
|
instance.SetCurrentTimeline(assetDoubleClicked);
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual void AddItemsToMenu(GenericMenu menu)
|
|
{
|
|
bool disabled = state == null || state.editSequence.asset == null;
|
|
|
|
m_LockTracker.AddItemsToMenu(menu, disabled);
|
|
}
|
|
|
|
protected virtual void ShowButton(Rect r)
|
|
{
|
|
bool disabled = state == null || state.editSequence.asset == null;
|
|
|
|
m_LockTracker.ShowButton(r, DirectorStyles.Instance.lockButton, disabled);
|
|
}
|
|
|
|
internal void TreeViewKeyboardCallback()
|
|
{
|
|
if (Event.current.type != EventType.KeyDown)
|
|
return;
|
|
if (TimelineAction.HandleShortcut(state, Event.current))
|
|
{
|
|
Event.current.Use();
|
|
}
|
|
}
|
|
}
|
|
}
|