using System; using System.Collections.Generic; using UnityEditor.Callbacks; using UnityEngine; using UnityEngine.Events; using UnityEngine.Playables; using UnityEngine.SceneManagement; using UnityEngine.Timeline; using Object = UnityEngine.Object; namespace UnityEditor.Timeline { internal interface IWindowStateProvider { IWindowState windowState { get; } } [EditorWindowTitle(title = "Timeline", useTypeNameAsIconName = true)] partial class TimelineWindow : TimelineEditorWindow, IHasCustomMenu, IWindowStateProvider { [Serializable] public class TimelineWindowPreferences { 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 allTracks { get { return treeView != null ? treeView.allTrackGuis : new List(); } } public WindowState state { get; private set; } IWindowState IWindowStateProvider.windowState => state; public override 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(); UpdateTitle(); 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 OnHierarchyChange() { hierarchyChangedThisFrame = true; Repaint(); } void OnStateChange() { state.UpdateRecordingState(); state.editSequence.InvalidateChildAssetCache(); 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); 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 (state != null && state.ignorePreview && state.playing) { 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) { var fillColor = new Color(1.0f, 1.0f, 1.0f, 0.1f); state.spacePartitioner.DebugDraw(fillColor, Color.yellow); state.headerSpacePartitioner.DebugDraw(fillColor, Color.green); } // 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; } if (Event.current.type == EventType.Layout) { UpdateTitle(); } } void UpdateTitle() { #if UNITY_2020_2_OR_NEWER bool dirty = false; List children = state?.editSequence.cachedChildAssets; if (children != null) { foreach (var child in children) { dirty = EditorUtility.IsDirty(child); if (dirty) { break; } } } hasUnsavedChanges = dirty; #endif } 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) ClearTimeline(); m_CurrentSceneHashCode = SceneManager.GetActiveScene().GetHashCode(); } } } void DetectStateChanges() { if (state != null) { foreach (var sequenceState in state.allSequences) { sequenceState.ResetIsReadOnly(); } // detect if the sequence was removed under our feet if (m_LastFrameHadSequence && state.editSequence.asset == null) { ClearTimeline(); } 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; if (locked) { //revert lock if the original context was not asset mode if (!state.masterSequence.isAssetOnly) locked = false; } 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() : null; if (selectedDirector != null) { SetTimeline(selectedDirector); } else { state.masterSequence.isAssetOnly = true; } } } else { // the user may have changed the timeline associated with the current director if (state.editSequence.asset != state.editSequence.director.playableAsset) { if (!locked) { SetTimeline(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 SetTimeline(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 || currentMode.mode != TimelineModes.Active || 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 (!state.ignorePreview) { 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 = TimelineUtility.CreateAndSaveTimelineAsset(pathName); ProjectWindowUtil.ShowCreatedAsset(timeline); } } [MenuItem("Assets/Create/Timeline/Timeline", false, -124)] public static void CreateNewTimeline() { var icon = EditorGUIUtility.IconContent("TimelineAsset Icon").image as Texture2D; ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, CreateInstance(), "New Timeline.playable", icon, null); } [MenuItem("Window/Sequencing/Timeline", false, 1)] public static void ShowWindow() { GetWindow(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.SetTimeline(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.timelineLockButton, disabled); } } }