A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using UnityEditor.SceneManagement;
5using UnityEditor.ShortcutManagement;
6using UnityEditor.Timeline.Actions;
7using UnityEngine;
8using UnityEngine.Animations;
9using UnityEngine.Playables;
10using UnityEngine.SceneManagement;
11using UnityEngine.Timeline;
12
13namespace UnityEditor.Timeline
14{
15 partial class TimelineWindow
16 {
17 private int m_ComponentAddedFrame;
18
19 void OnSelectionChangedInactive()
20 {
21 // Case 946942 -- when selection changes and the window is open but hidden, timeline
22 // needs to update selection immediately so preview mode is correctly released
23 // Case 1123119 -- except when recording
24 if (!hasFocus)
25 {
26 RefreshSelection(!locked && state != null && !state.recording);
27 }
28 }
29
30 void InitializeEditorCallbacks()
31 {
32 Undo.postprocessModifications += PostprocessAnimationRecordingModifications;
33 Undo.postprocessModifications += ProcessAssetModifications;
34 Undo.undoRedoPerformed += OnUndoRedo;
35 EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
36 AnimationUtility.onCurveWasModified += OnCurveModified;
37 EditorApplication.editorApplicationQuit += OnEditorQuit;
38 Selection.selectionChanged += OnSelectionChangedInactive;
39 EditorSceneManager.sceneSaved += OnSceneSaved;
40 ObjectFactory.componentWasAdded += OnComponentWasAdded;
41 PrefabUtility.prefabInstanceUpdated += OnPrefabApplied;
42 EditorApplication.pauseStateChanged += OnPlayModePause;
43 EditorApplication.globalEventHandler += GlobalEventHandler;
44#if TIMELINE_FRAMEACCURATE
45 TimelinePlayable.playableLooped += OnPlayableLooped;
46#endif
47 }
48
49 // This callback is needed because the Animation window registers "Animation/Key Selected" as a global hotkey
50 // and we want to also react to the key.
51 void GlobalEventHandler()
52 {
53 if (instance == null || !state.previewMode)
54 {
55 return;
56 }
57
58 var keyBinding = ShortcutManager.instance.GetShortcutBinding("Animation/Key Selected");
59 if (keyBinding.Equals(ShortcutBinding.empty))
60 {
61 return;
62 }
63
64 var evtCombo = KeyCombination.FromKeyboardInput(Event.current);
65 if (keyBinding.keyCombinationSequence.Contains(evtCombo))
66 {
67 Invoker.InvokeWithSelected<KeyAllAnimated>();
68 }
69 }
70
71 void OnEditorQuit()
72 {
73 TimelineWindowViewPrefs.SaveAll();
74 }
75
76 void RemoveEditorCallbacks()
77 {
78 EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
79
80 Undo.undoRedoPerformed -= OnUndoRedo;
81 Undo.postprocessModifications -= PostprocessAnimationRecordingModifications;
82 Undo.postprocessModifications -= ProcessAssetModifications;
83 AnimationUtility.onCurveWasModified -= OnCurveModified;
84 EditorApplication.editorApplicationQuit -= OnEditorQuit;
85 Selection.selectionChanged -= OnSelectionChangedInactive;
86 EditorSceneManager.sceneSaved -= OnSceneSaved;
87 ObjectFactory.componentWasAdded -= OnComponentWasAdded;
88 PrefabUtility.prefabInstanceUpdated -= OnPrefabApplied;
89 EditorApplication.pauseStateChanged -= OnPlayModePause;
90 EditorApplication.globalEventHandler -= GlobalEventHandler;
91#if TIMELINE_FRAMEACCURATE
92 TimelinePlayable.playableLooped -= OnPlayableLooped;
93#endif
94 }
95
96 void OnPlayModePause(PauseState state)
97 {
98 // in PlayMode, if the timeline is playing, a constant repaint cycle occurs. Pausing the editor
99 // breaks the cycle, so this will restart it
100 Repaint();
101 }
102
103 // Called when a prefab change is applied to the scene.
104 // Redraw so control tracks that use prefabs can show changes
105 void OnPrefabApplied(GameObject go)
106 {
107 if (!state.previewMode)
108 return;
109
110 // if we added a component this frame, then rebuild, otherwise just let
111 // the individual playable handle the prefab application
112 if (Time.frameCount == m_ComponentAddedFrame)
113 TimelineEditor.Refresh(RefreshReason.ContentsModified);
114 else
115 TimelineEditor.Refresh(RefreshReason.SceneNeedsUpdate);
116 }
117
118 // When the scene is save the director time will get reset.
119 void OnSceneSaved(Scene scene)
120 {
121 if (state != null)
122 state.OnSceneSaved();
123 }
124
125 void OnCurveModified(AnimationClip clip, EditorCurveBinding binding, AnimationUtility.CurveModifiedType type)
126 {
127 InspectorWindow.RepaintAllInspectors();
128 if (state == null)
129 return;
130
131 //Force refresh of curve when modified by another editor.
132 Repaint();
133
134 if (state.previewMode == false)
135 return;
136
137 bool hasPlayable = m_PlayableLookup.GetPlayableFromAnimClip(clip, out Playable playable);
138
139 // mark the timeline clip as dirty
140 TimelineClip timelineClip = m_PlayableLookup.GetTimelineClipFromCurves(clip);
141 if (timelineClip != null)
142 timelineClip.MarkDirty();
143
144 if (type == AnimationUtility.CurveModifiedType.CurveModified)
145 {
146 if (hasPlayable)
147 {
148 playable.SetAnimatedProperties(clip);
149 }
150
151 // updates the duration of the graph without rebuilding
152 AnimationUtility.SyncEditorCurves(clip); // deleted keys are not synced when this is sent out, so duration could be incorrect
153 state.UpdateRootPlayableDuration(state.editSequence.duration);
154
155 bool isRecording = TimelineRecording.IsRecordingAnimationTrack;
156 PlayableDirector masterDirector = TimelineEditor.masterDirector;
157 bool isGraphValid = masterDirector != null && masterDirector.playableGraph.IsValid();
158
159 // don't evaluate if this is caused by recording on an animation track, the extra evaluation can cause hiccups
160 // Prevent graphs to be resurrected by a changed clip.
161 if (!isRecording && isGraphValid)
162 state.Evaluate();
163 }
164 else if (EditorUtility.IsDirty(clip)) // curve added/removed, or clip added/removed
165 {
166 state.rebuildGraph |= timelineClip != null || hasPlayable;
167 }
168 }
169
170 void OnPlayModeStateChanged(PlayModeStateChange playModeState)
171 {
172 // case 923506 - make sure we save view data before switching modes
173 if (playModeState == PlayModeStateChange.ExitingEditMode ||
174 playModeState == PlayModeStateChange.ExitingPlayMode)
175 TimelineWindowViewPrefs.SaveAll();
176
177 bool isPlaymodeAboutToChange = playModeState == PlayModeStateChange.ExitingEditMode || playModeState == PlayModeStateChange.ExitingPlayMode;
178
179 // Important to stop the graph on any director so temporary objects are properly cleaned up
180 if (isPlaymodeAboutToChange && state != null)
181 state.Stop();
182 }
183
184 UndoPropertyModification[] PostprocessAnimationRecordingModifications(UndoPropertyModification[] modifications)
185 {
186 DirtyModifiedObjects(modifications);
187
188 var remaining = TimelineRecording.ProcessUndoModification(modifications, state);
189 // if we've changed, we need to repaint the sequence window to show clip length changes
190 if (remaining != modifications)
191 {
192 // only update if us or the sequencer window has focus
193 // Prevents color pickers and other dialogs from being wrongly dismissed
194 bool repaint = (focusedWindow == null) ||
195 (focusedWindow is InspectorWindow) ||
196 (focusedWindow is TimelineWindow);
197
198 if (repaint)
199 Repaint();
200 }
201
202
203 return remaining;
204 }
205
206 void DirtyModifiedObjects(UndoPropertyModification[] modifications)
207 {
208 foreach (var m in modifications)
209 {
210 if (m.currentValue == null || m.currentValue.target == null)
211 continue;
212
213 var track = m.currentValue.target as TrackAsset;
214 var playableAsset = m.currentValue.target as PlayableAsset;
215 var editorClip = m.currentValue.target as EditorClip;
216
217 if (track != null)
218 {
219 track.MarkDirtyTrackAndClips();
220 }
221 else if (playableAsset != null)
222 {
223 var clip = TimelineRecording.FindClipWithAsset(state.editSequence.asset, playableAsset);
224 if (clip != null)
225 {
226 clip.MarkDirty();
227 }
228 }
229 else if (editorClip != null && editorClip.clip != null)
230 {
231 editorClip.clip.MarkDirty();
232 }
233 }
234 }
235
236 UndoPropertyModification[] ProcessAssetModifications(UndoPropertyModification[] modifications)
237 {
238 bool rebuildGraph = false;
239
240 for (int i = 0; i < modifications.Length && !rebuildGraph; i++)
241 {
242 var mod = modifications[i];
243
244 if (mod.currentValue != null && mod.currentValue.target is IMarker currentMarker)
245 {
246 if (currentMarker.parent != null && currentMarker.parent.timelineAsset == state.editSequence.asset)
247 {
248 if (mod.currentValue.target is INotification)
249 TimelineEditor.Refresh(RefreshReason.ContentsModified);
250 else
251 TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
252 }
253 }
254 else if (mod.previousValue != null && mod.previousValue.target is AvatarMask) // check if an Avatar Mask has been modified
255 {
256 rebuildGraph = state.editSequence.asset != null &&
257 state.editSequence.asset.flattenedTracks
258 .OfType<UnityEngine.Timeline.AnimationTrack>()
259 .Any(x => mod.previousValue.target == x.avatarMask);
260 }
261 }
262
263 if (rebuildGraph)
264 {
265 state.rebuildGraph = true;
266 Repaint();
267 }
268
269 return modifications;
270 }
271
272 void OnUndoRedo()
273 {
274 var undos = new List<string>();
275 var redos = new List<string>();
276 Undo.GetRecords(undos, redos);
277
278 var rebuildAll = redos.Any(x => x.StartsWith("Timeline ")) || undos.Any(x => x.StartsWith("Timeline"));
279 var evalNow = redos.Any(x => x.Contains("Edit Curve")) || undos.Any(x => x.Contains("Edit Curve"));
280 if (rebuildAll || evalNow)
281 {
282 ValidateSelection();
283 if (state != null)
284 {
285 if (evalNow) // when curves change, the new values need to be set in the transform before the inspector handles the undo
286 state.EvaluateImmediate();
287 if (rebuildAll)
288 state.Refresh();
289 }
290 Repaint();
291 }
292 }
293
294 static void ValidateSelection()
295 {
296 //get all the clips in the selection
297 var selectedClips = Selection.GetFiltered<EditorClip>(SelectionMode.Unfiltered).Select(x => x.clip);
298 foreach (var selectedClip in selectedClips)
299 {
300 var parent = selectedClip.GetParentTrack();
301 if (selectedClip.GetParentTrack() != null)
302 {
303 if (!parent.clips.Contains(selectedClip))
304 {
305 SelectionManager.Remove(selectedClip);
306 }
307 }
308 }
309 }
310
311 void OnComponentWasAdded(Component c)
312 {
313 m_ComponentAddedFrame = Time.frameCount;
314 var go = c.gameObject;
315 foreach (var seq in state.GetAllSequences())
316 {
317 if (seq.director == null || seq.asset == null)
318 {
319 return;
320 }
321
322 var rebind = seq.asset.GetOutputTracks().Any(track => seq.director.GetGenericBinding(track) == go);
323 // Either the playable director has a binding for the GameObject or it is a sibling of the director.
324 // The second case is needed since we have timeline top level markerTracks that do not have a binding, but
325 // are still "targeting" the playable director
326 if (rebind || seq.director.gameObject == go)
327 {
328 seq.director.RebindPlayableGraphOutputs();
329 }
330 }
331 }
332
333#if TIMELINE_FRAMEACCURATE
334 void OnPlayableLooped(Playable timelinePlayable)
335 {
336 if (state == null || !state.playing || state.masterSequence == null || state.masterSequence.director == null
337 || !state.masterSequence.director.playableGraph.IsValid())
338 return;
339 var masterPlayable = state.masterSequence.director.playableGraph.GetRootPlayable(0);
340 if (!masterPlayable.Equals(Playable.Null)
341 && masterPlayable.Equals(timelinePlayable)
342 && timelinePlayable.GetGraph().IsMatchFrameRateEnabled())
343 timelinePlayable.SetTime(0);
344 }
345
346#endif
347 }
348}