A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEditor.Callbacks;
4using UnityEngine;
5using UnityEngine.Events;
6using UnityEngine.Playables;
7using UnityEngine.SceneManagement;
8using UnityEngine.Timeline;
9using Object = UnityEngine.Object;
10
11namespace UnityEditor.Timeline
12{
13 internal interface IWindowStateProvider
14 {
15 IWindowState windowState { get; }
16 }
17
18 [EditorWindowTitle(title = "Timeline", useTypeNameAsIconName = true)]
19 partial class TimelineWindow : TimelineEditorWindow, IHasCustomMenu, IWindowStateProvider
20 {
21 [Serializable]
22 public class TimelineWindowPreferences
23 {
24 public EditMode.EditType editType = EditMode.EditType.Mix;
25 public TimeReferenceMode timeReferenceMode = TimeReferenceMode.Local;
26 }
27
28 [SerializeField] TimelineWindowPreferences m_Preferences = new TimelineWindowPreferences();
29 public TimelineWindowPreferences preferences { get { return m_Preferences; } }
30
31 [SerializeField]
32 EditorGUIUtility.EditorLockTracker m_LockTracker = new EditorGUIUtility.EditorLockTracker();
33
34 readonly PreviewResizer m_PreviewResizer = new PreviewResizer();
35 bool m_LastFrameHadSequence;
36 bool m_ForceRefreshLastSelection;
37 int m_CurrentSceneHashCode = -1;
38
39 [NonSerialized]
40 bool m_HasBeenInitialized;
41
42 [SerializeField]
43 SequenceHierarchy m_SequenceHierarchy;
44 static SequenceHierarchy s_LastHierarchy;
45
46 public static TimelineWindow instance { get; private set; }
47 public Rect clientArea { get; set; }
48 public bool isDragging { get; set; }
49 public static DirectorStyles styles { get { return DirectorStyles.Instance; } }
50 public List<TimelineTrackBaseGUI> allTracks
51 {
52 get
53 {
54 return treeView != null ? treeView.allTrackGuis : new List<TimelineTrackBaseGUI>();
55 }
56 }
57
58 public WindowState state { get; private set; }
59
60 IWindowState IWindowStateProvider.windowState => state;
61
62 public override bool locked
63 {
64 get
65 {
66 // we can never be in a locked state if there is no timeline asset
67 if (state.editSequence.asset == null)
68 return false;
69
70 return m_LockTracker.isLocked;
71 }
72 set { m_LockTracker.isLocked = value; }
73 }
74
75 public bool hierarchyChangedThisFrame { get; private set; }
76
77 public TimelineWindow()
78 {
79 InitializeManipulators();
80 m_LockTracker.lockStateChanged.AddPersistentListener(OnLockStateChanged, UnityEventCallState.EditorAndRuntime);
81 }
82
83 void OnLockStateChanged(bool locked)
84 {
85 // Make sure that upon unlocking, any selection change is updated
86 // Case 1123119 -- only force rebuild if not recording
87 if (!locked)
88 RefreshSelection(state != null && !state.recording);
89 }
90
91 void OnEnable()
92 {
93 if (m_SequencePath == null)
94 m_SequencePath = new SequencePath();
95
96 if (m_SequenceHierarchy == null)
97 {
98 // The sequence hierarchy will become null if maximize on play is used for in/out of playmode
99 // a static var will hang on to the reference
100 if (s_LastHierarchy != null)
101 m_SequenceHierarchy = s_LastHierarchy;
102 else
103 m_SequenceHierarchy = SequenceHierarchy.CreateInstance();
104
105 state = null;
106 }
107 s_LastHierarchy = m_SequenceHierarchy;
108
109 titleContent = GetLocalizedTitleContent();
110
111 UpdateTitle();
112
113 m_PreviewResizer.Init("TimelineWindow");
114
115 // Unmaximize fix : when unmaximizing, a new window is enabled and disabled. Prevent it from overriding the instance pointer.
116 if (instance == null)
117 instance = this;
118
119 AnimationClipCurveCache.Instance.OnEnable();
120 TrackAsset.OnClipPlayableCreate += m_PlayableLookup.UpdatePlayableLookup;
121 TrackAsset.OnTrackAnimationPlayableCreate += m_PlayableLookup.UpdatePlayableLookup;
122
123 if (state == null)
124 {
125 state = new WindowState(this, s_LastHierarchy);
126 Initialize();
127 RefreshSelection(true);
128 m_ForceRefreshLastSelection = true;
129 }
130 }
131
132 void OnDisable()
133 {
134 if (instance == this)
135 instance = null;
136
137 if (state != null)
138 state.Reset();
139
140 if (instance == null)
141 SelectionManager.RemoveTimelineSelection();
142
143 AnimationClipCurveCache.Instance.OnDisable();
144 TrackAsset.OnClipPlayableCreate -= m_PlayableLookup.UpdatePlayableLookup;
145 TrackAsset.OnTrackAnimationPlayableCreate -= m_PlayableLookup.UpdatePlayableLookup;
146 TimelineWindowViewPrefs.SaveAll();
147 TimelineWindowViewPrefs.UnloadAllViewModels();
148 }
149
150 void OnDestroy()
151 {
152 if (state != null)
153 {
154 state.OnDestroy();
155 }
156 m_HasBeenInitialized = false;
157 RemoveEditorCallbacks();
158 AnimationClipCurveCache.Instance.Clear();
159 TimelineAnimationUtilities.UnlinkAnimationWindow();
160 }
161
162 void OnLostFocus()
163 {
164 isDragging = false;
165
166 if (state != null)
167 state.captured.Clear();
168
169 Repaint();
170 }
171
172 void OnHierarchyChange()
173 {
174 hierarchyChangedThisFrame = true;
175 Repaint();
176 }
177
178 void OnStateChange()
179 {
180 state.UpdateRecordingState();
181 state.editSequence.InvalidateChildAssetCache();
182 if (treeView != null && state.editSequence.asset != null)
183 treeView.Reload();
184 if (m_MarkerHeaderGUI != null)
185 m_MarkerHeaderGUI.Rebuild();
186 }
187
188 void OnGUI()
189 {
190 InitializeGUIIfRequired();
191 UpdateGUIConstants();
192 UpdateViewStateHash();
193
194 EditMode.HandleModeClutch(); // TODO We Want that here?
195
196 DetectStylesChange();
197 DetectActiveSceneChanges();
198 DetectStateChanges();
199
200 state.ProcessStartFramePendingUpdates();
201
202 var clipRect = new Rect(0.0f, 0.0f, position.width, position.height);
203
204 using (new GUIViewportScope(clipRect))
205 state.InvokeWindowOnGuiStarted(Event.current);
206
207 if (Event.current.type == EventType.MouseDrag && state != null && state.mouseDragLag > 0.0f)
208 {
209 state.mouseDragLag -= Time.deltaTime;
210 return;
211 }
212
213 if (PerformUndo())
214 return;
215
216 if (state != null && state.ignorePreview && state.playing)
217 {
218 if (state.recording)
219 state.recording = false;
220 Repaint();
221 }
222
223 clientArea = position;
224
225 PlaybackScroller.AutoScroll(state);
226 DoLayout();
227
228 // overlays
229 if (state.captured.Count > 0)
230 {
231 using (new GUIViewportScope(clipRect))
232 {
233 foreach (var o in state.captured)
234 {
235 o.Overlay(Event.current, state);
236 }
237 Repaint();
238 }
239 }
240
241 if (state.showQuadTree)
242 {
243 var fillColor = new Color(1.0f, 1.0f, 1.0f, 0.1f);
244 state.spacePartitioner.DebugDraw(fillColor, Color.yellow);
245 state.headerSpacePartitioner.DebugDraw(fillColor, Color.green);
246 }
247
248 // attempt another rebuild -- this will avoid 1 frame flashes
249 if (Event.current.type == EventType.Repaint)
250 {
251 RebuildGraphIfNecessary();
252 state.ProcessEndFramePendingUpdates();
253 }
254
255 using (new GUIViewportScope(clipRect))
256 {
257 if (Event.current.type == EventType.Repaint)
258 EditMode.inputHandler.OnGUI(state, Event.current);
259 }
260
261 if (Event.current.type == EventType.Repaint)
262 {
263 hierarchyChangedThisFrame = false;
264 }
265
266 if (Event.current.type == EventType.Layout)
267 {
268 UpdateTitle();
269 }
270 }
271
272 void UpdateTitle()
273 {
274#if UNITY_2020_2_OR_NEWER
275 bool dirty = false;
276 List<Object> children = state?.editSequence.cachedChildAssets;
277 if (children != null)
278 {
279 foreach (var child in children)
280 {
281 dirty = EditorUtility.IsDirty(child);
282 if (dirty)
283 {
284 break;
285 }
286 }
287 }
288
289 hasUnsavedChanges = dirty;
290#endif
291 }
292
293 static void DetectStylesChange()
294 {
295 DirectorStyles.ReloadStylesIfNeeded();
296 }
297
298 void DetectActiveSceneChanges()
299 {
300 if (m_CurrentSceneHashCode == -1)
301 {
302 m_CurrentSceneHashCode = SceneManager.GetActiveScene().GetHashCode();
303 }
304
305 if (m_CurrentSceneHashCode != SceneManager.GetActiveScene().GetHashCode())
306 {
307 bool isSceneStillLoaded = false;
308 for (int a = 0; a < SceneManager.sceneCount; a++)
309 {
310 var scene = SceneManager.GetSceneAt(a);
311 if (scene.GetHashCode() == m_CurrentSceneHashCode && scene.isLoaded)
312 {
313 isSceneStillLoaded = true;
314 break;
315 }
316 }
317
318 if (!isSceneStillLoaded)
319 {
320 if (!locked)
321 ClearTimeline();
322 m_CurrentSceneHashCode = SceneManager.GetActiveScene().GetHashCode();
323 }
324 }
325 }
326
327 void DetectStateChanges()
328 {
329 if (state != null)
330 {
331 foreach (var sequenceState in state.allSequences)
332 {
333 sequenceState.ResetIsReadOnly();
334 }
335 // detect if the sequence was removed under our feet
336 if (m_LastFrameHadSequence && state.editSequence.asset == null)
337 {
338 ClearTimeline();
339 }
340 m_LastFrameHadSequence = state.editSequence.asset != null;
341
342 // the currentDirector can get set to null by a deletion or scene unloading so polling is required
343 if (state.editSequence.director == null)
344 {
345 state.recording = false;
346 state.previewMode = false;
347
348 if (locked)
349 {
350 //revert lock if the original context was not asset mode
351 if (!state.masterSequence.isAssetOnly)
352 locked = false;
353 }
354
355 if (!locked && m_LastFrameHadSequence)
356 {
357 // 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
358 var selectedGameObject = Selection.activeObject != null ? Selection.activeObject as GameObject : null;
359 var selectedDirector = selectedGameObject != null ? selectedGameObject.GetComponent<PlayableDirector>() : null;
360 if (selectedDirector != null)
361 {
362 SetTimeline(selectedDirector);
363 }
364 else
365 {
366 state.masterSequence.isAssetOnly = true;
367 }
368 }
369 }
370 else
371 {
372 // the user may have changed the timeline associated with the current director
373 if (state.editSequence.asset != state.editSequence.director.playableAsset)
374 {
375 if (!locked)
376 {
377 SetTimeline(state.editSequence.director);
378 }
379 else
380 {
381 // Keep locked on the current timeline but set the current director to null since it's not the timeline owner anymore
382 SetTimeline(state.editSequence.asset);
383 }
384 }
385 }
386 }
387 }
388
389 void Initialize()
390 {
391 if (!m_HasBeenInitialized)
392 {
393 InitializeStateChange();
394 InitializeEditorCallbacks();
395 m_HasBeenInitialized = true;
396 }
397 }
398
399 void RefreshLastSelectionIfRequired()
400 {
401 // case 1088918 - workaround for the instanceID to object cache being update during Awake.
402 // This corrects any playableDirector ptrs with the correct cached version
403 // This can happen when going from edit to playmode
404 if (m_ForceRefreshLastSelection)
405 {
406 m_ForceRefreshLastSelection = false;
407 RestoreLastSelection(true);
408 }
409 }
410
411 void InitializeGUIIfRequired()
412 {
413 RefreshLastSelectionIfRequired();
414 InitializeTimeArea();
415 if (treeView == null && state.editSequence.asset != null)
416 {
417 treeView = new TimelineTreeViewGUI(this, state.editSequence.asset, position);
418 }
419 }
420
421 void UpdateGUIConstants()
422 {
423 m_HorizontalScrollBarSize =
424 GUI.skin.horizontalScrollbar.fixedHeight + GUI.skin.horizontalScrollbar.margin.top;
425 m_VerticalScrollBarSize = (treeView != null && treeView.showingVerticalScrollBar)
426 ? GUI.skin.verticalScrollbar.fixedWidth + GUI.skin.verticalScrollbar.margin.left
427 : 0;
428 }
429
430 void UpdateViewStateHash()
431 {
432 if (Event.current.type == EventType.Layout)
433 state.UpdateViewStateHash();
434 }
435
436 static bool PerformUndo()
437 {
438 if (!Event.current.isKey)
439 return false;
440
441 if (Event.current.keyCode != KeyCode.Z)
442 return false;
443
444 if (!EditorGUI.actionKey)
445 return false;
446
447 return true;
448 }
449
450 public void RebuildGraphIfNecessary(bool evaluate = true)
451 {
452 if (state == null || currentMode.mode != TimelineModes.Active || state.editSequence.director == null || state.editSequence.asset == null)
453 return;
454
455 if (state.rebuildGraph)
456 {
457 // rebuilding the graph resets the time
458 double time = state.editSequence.time;
459
460 var wasPlaying = false;
461
462 // disable preview mode,
463 if (!state.ignorePreview)
464 {
465 wasPlaying = state.playing;
466
467 state.previewMode = false;
468 state.GatherProperties(state.masterSequence.director);
469 }
470 state.RebuildPlayableGraph();
471 state.editSequence.time = time;
472
473 if (wasPlaying)
474 state.Play();
475
476 if (evaluate)
477 {
478 // put the scene back in the correct state
479 state.EvaluateImmediate();
480
481 // this is necessary to see accurate results when inspector refreshes
482 // case 1154802 - this will property re-force time on the director, so
483 // the play head won't snap back to the timeline duration on rebuilds
484 if (!state.playing)
485 state.Evaluate();
486 }
487 Repaint();
488 }
489
490 state.rebuildGraph = false;
491 }
492
493 // for tests
494 public new void RepaintImmediately()
495 {
496 base.RepaintImmediately();
497 }
498
499 internal static bool IsEditingTimelineAsset(TimelineAsset timelineAsset)
500 {
501 return instance != null && instance.state != null && instance.state.editSequence.asset == timelineAsset;
502 }
503
504 internal static void RepaintIfEditingTimelineAsset(TimelineAsset timelineAsset)
505 {
506 if (IsEditingTimelineAsset(timelineAsset))
507 instance.Repaint();
508 }
509
510 internal class DoCreateTimeline : ProjectWindowCallback.EndNameEditAction
511 {
512 public override void Action(int instanceId, string pathName, string resourceFile)
513 {
514 var timeline = TimelineUtility.CreateAndSaveTimelineAsset(pathName);
515 ProjectWindowUtil.ShowCreatedAsset(timeline);
516 }
517 }
518
519 [MenuItem("Assets/Create/Timeline/Timeline", false, -124)]
520 public static void CreateNewTimeline()
521 {
522 var icon = EditorGUIUtility.IconContent("TimelineAsset Icon").image as Texture2D;
523 ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, CreateInstance<DoCreateTimeline>(), "New Timeline.playable", icon, null);
524 }
525
526 [MenuItem("Window/Sequencing/Timeline", false, 1)]
527 public static void ShowWindow()
528 {
529 GetWindow<TimelineWindow>(typeof(SceneView));
530 instance.Focus();
531 }
532
533 [OnOpenAsset(1)]
534 public static bool OnDoubleClick(int instanceID, int line)
535 {
536 var assetDoubleClicked = EditorUtility.InstanceIDToObject(instanceID) as TimelineAsset;
537 if (assetDoubleClicked == null)
538 return false;
539
540 ShowWindow();
541 instance.SetTimeline(assetDoubleClicked);
542
543 return true;
544 }
545
546 public virtual void AddItemsToMenu(GenericMenu menu)
547 {
548 bool disabled = state == null || state.editSequence.asset == null;
549
550 m_LockTracker.AddItemsToMenu(menu, disabled);
551 }
552
553 protected virtual void ShowButton(Rect r)
554 {
555 bool disabled = state == null || state.editSequence.asset == null;
556
557 m_LockTracker.ShowButton(r, DirectorStyles.Instance.timelineLockButton, disabled);
558 }
559 }
560}