You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
560 lines
18 KiB
560 lines
18 KiB
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<TimelineTrackBaseGUI> allTracks |
|
{ |
|
get |
|
{ |
|
return treeView != null ? treeView.allTrackGuis : new List<TimelineTrackBaseGUI>(); |
|
} |
|
} |
|
|
|
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<Object> 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<PlayableDirector>() : 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", 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.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); |
|
} |
|
} |
|
}
|
|
|