using System; using System.Collections.Generic; using System.Linq; using UnityEditor.SceneManagement; using UnityEditor.ShortcutManagement; using UnityEditor.Timeline.Actions; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; using UnityEngine.SceneManagement; using UnityEngine.Timeline; namespace UnityEditor.Timeline { partial class TimelineWindow { private int m_ComponentAddedFrame; void OnSelectionChangedInactive() { // Case 946942 -- when selection changes and the window is open but hidden, timeline // needs to update selection immediately so preview mode is correctly released // Case 1123119 -- except when recording if (!hasFocus) { RefreshSelection(!locked && state != null && !state.recording); } } void InitializeEditorCallbacks() { Undo.postprocessModifications += PostprocessAnimationRecordingModifications; Undo.postprocessModifications += ProcessAssetModifications; Undo.undoRedoPerformed += OnUndoRedo; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; AnimationUtility.onCurveWasModified += OnCurveModified; EditorApplication.editorApplicationQuit += OnEditorQuit; Selection.selectionChanged += OnSelectionChangedInactive; EditorSceneManager.sceneSaved += OnSceneSaved; ObjectFactory.componentWasAdded += OnComponentWasAdded; PrefabUtility.prefabInstanceUpdated += OnPrefabApplied; EditorApplication.pauseStateChanged += OnPlayModePause; EditorApplication.globalEventHandler += GlobalEventHandler; #if TIMELINE_FRAMEACCURATE TimelinePlayable.playableLooped += OnPlayableLooped; #endif } // This callback is needed because the Animation window registers "Animation/Key Selected" as a global hotkey // and we want to also react to the key. void GlobalEventHandler() { if (instance == null || !state.previewMode) { return; } var keyBinding = ShortcutManager.instance.GetShortcutBinding("Animation/Key Selected"); if (keyBinding.Equals(ShortcutBinding.empty)) { return; } var evtCombo = KeyCombination.FromKeyboardInput(Event.current); if (keyBinding.keyCombinationSequence.Contains(evtCombo)) { Invoker.InvokeWithSelected(); } } void OnEditorQuit() { TimelineWindowViewPrefs.SaveAll(); } void RemoveEditorCallbacks() { EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; Undo.undoRedoPerformed -= OnUndoRedo; Undo.postprocessModifications -= PostprocessAnimationRecordingModifications; Undo.postprocessModifications -= ProcessAssetModifications; AnimationUtility.onCurveWasModified -= OnCurveModified; EditorApplication.editorApplicationQuit -= OnEditorQuit; Selection.selectionChanged -= OnSelectionChangedInactive; EditorSceneManager.sceneSaved -= OnSceneSaved; ObjectFactory.componentWasAdded -= OnComponentWasAdded; PrefabUtility.prefabInstanceUpdated -= OnPrefabApplied; EditorApplication.pauseStateChanged -= OnPlayModePause; EditorApplication.globalEventHandler -= GlobalEventHandler; #if TIMELINE_FRAMEACCURATE TimelinePlayable.playableLooped -= OnPlayableLooped; #endif } void OnPlayModePause(PauseState state) { // in PlayMode, if the timeline is playing, a constant repaint cycle occurs. Pausing the editor // breaks the cycle, so this will restart it Repaint(); } // Called when a prefab change is applied to the scene. // Redraw so control tracks that use prefabs can show changes void OnPrefabApplied(GameObject go) { if (!state.previewMode) return; // if we added a component this frame, then rebuild, otherwise just let // the individual playable handle the prefab application if (Time.frameCount == m_ComponentAddedFrame) TimelineEditor.Refresh(RefreshReason.ContentsModified); else TimelineEditor.Refresh(RefreshReason.SceneNeedsUpdate); } // When the scene is save the director time will get reset. void OnSceneSaved(Scene scene) { if (state != null) state.OnSceneSaved(); } void OnCurveModified(AnimationClip clip, EditorCurveBinding binding, AnimationUtility.CurveModifiedType type) { InspectorWindow.RepaintAllInspectors(); if (state == null) return; //Force refresh of curve when modified by another editor. Repaint(); if (state.previewMode == false) return; bool hasPlayable = m_PlayableLookup.GetPlayableFromAnimClip(clip, out Playable playable); // mark the timeline clip as dirty TimelineClip timelineClip = m_PlayableLookup.GetTimelineClipFromCurves(clip); if (timelineClip != null) timelineClip.MarkDirty(); if (type == AnimationUtility.CurveModifiedType.CurveModified) { if (hasPlayable) { playable.SetAnimatedProperties(clip); } // updates the duration of the graph without rebuilding AnimationUtility.SyncEditorCurves(clip); // deleted keys are not synced when this is sent out, so duration could be incorrect state.UpdateRootPlayableDuration(state.editSequence.duration); bool isRecording = TimelineRecording.IsRecordingAnimationTrack; PlayableDirector masterDirector = TimelineEditor.masterDirector; bool isGraphValid = masterDirector != null && masterDirector.playableGraph.IsValid(); // don't evaluate if this is caused by recording on an animation track, the extra evaluation can cause hiccups // Prevent graphs to be resurrected by a changed clip. if (!isRecording && isGraphValid) state.Evaluate(); } else if (EditorUtility.IsDirty(clip)) // curve added/removed, or clip added/removed { state.rebuildGraph |= timelineClip != null || hasPlayable; } } void OnPlayModeStateChanged(PlayModeStateChange playModeState) { // case 923506 - make sure we save view data before switching modes if (playModeState == PlayModeStateChange.ExitingEditMode || playModeState == PlayModeStateChange.ExitingPlayMode) TimelineWindowViewPrefs.SaveAll(); bool isPlaymodeAboutToChange = playModeState == PlayModeStateChange.ExitingEditMode || playModeState == PlayModeStateChange.ExitingPlayMode; // Important to stop the graph on any director so temporary objects are properly cleaned up if (isPlaymodeAboutToChange && state != null) state.Stop(); } UndoPropertyModification[] PostprocessAnimationRecordingModifications(UndoPropertyModification[] modifications) { DirtyModifiedObjects(modifications); var remaining = TimelineRecording.ProcessUndoModification(modifications, state); // if we've changed, we need to repaint the sequence window to show clip length changes if (remaining != modifications) { // only update if us or the sequencer window has focus // Prevents color pickers and other dialogs from being wrongly dismissed bool repaint = (focusedWindow == null) || (focusedWindow is InspectorWindow) || (focusedWindow is TimelineWindow); if (repaint) Repaint(); } return remaining; } void DirtyModifiedObjects(UndoPropertyModification[] modifications) { foreach (var m in modifications) { if (m.currentValue == null || m.currentValue.target == null) continue; var track = m.currentValue.target as TrackAsset; var playableAsset = m.currentValue.target as PlayableAsset; var editorClip = m.currentValue.target as EditorClip; if (track != null) { track.MarkDirty(); } else if (playableAsset != null) { var clip = TimelineRecording.FindClipWithAsset(state.editSequence.asset, playableAsset); if (clip != null) { clip.MarkDirty(); } } else if (editorClip != null && editorClip.clip != null) { editorClip.clip.MarkDirty(); } } } UndoPropertyModification[] ProcessAssetModifications(UndoPropertyModification[] modifications) { bool rebuildGraph = false; for (int i = 0; i < modifications.Length && !rebuildGraph; i++) { var mod = modifications[i]; if (mod.currentValue != null && mod.currentValue.target is IMarker currentMarker) { if (currentMarker.parent != null && currentMarker.parent.timelineAsset == state.editSequence.asset) { if (mod.currentValue.target is INotification) TimelineEditor.Refresh(RefreshReason.ContentsModified); else TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw); } } else if (mod.previousValue != null && mod.previousValue.target is AvatarMask) // check if an Avatar Mask has been modified { rebuildGraph = state.editSequence.asset != null && state.editSequence.asset.flattenedTracks .OfType() .Any(x => mod.previousValue.target == x.avatarMask); } } if (rebuildGraph) { state.rebuildGraph = true; Repaint(); } return modifications; } void OnUndoRedo() { var undos = new List(); var redos = new List(); Undo.GetRecords(undos, redos); var rebuildAll = redos.Any(x => x.StartsWith("Timeline ")) || undos.Any(x => x.StartsWith("Timeline")); var evalNow = redos.Any(x => x.Contains("Edit Curve")) || undos.Any(x => x.Contains("Edit Curve")); if (rebuildAll || evalNow) { ValidateSelection(); if (state != null) { if (evalNow) // when curves change, the new values need to be set in the transform before the inspector handles the undo state.EvaluateImmediate(); if (rebuildAll) state.Refresh(); } Repaint(); } } static void ValidateSelection() { //get all the clips in the selection var selectedClips = Selection.GetFiltered(SelectionMode.Unfiltered).Select(x => x.clip); foreach (var selectedClip in selectedClips) { var parent = selectedClip.GetParentTrack(); if (selectedClip.GetParentTrack() != null) { if (!parent.clips.Contains(selectedClip)) { SelectionManager.Remove(selectedClip); } } } } void OnComponentWasAdded(Component c) { m_ComponentAddedFrame = Time.frameCount; var go = c.gameObject; foreach (var seq in state.GetAllSequences()) { if (seq.director == null || seq.asset == null) { return; } var rebind = seq.asset.GetOutputTracks().Any(track => seq.director.GetGenericBinding(track) == go); // Either the playable director has a binding for the GameObject or it is a sibling of the director. // The second case is needed since we have timeline top level markerTracks that do not have a binding, but // are still "targeting" the playable director if (rebind || seq.director.gameObject == go) { seq.director.RebindPlayableGraphOutputs(); } } } #if TIMELINE_FRAMEACCURATE void OnPlayableLooped(Playable timelinePlayable) { if (state == null || !state.playing || state.masterSequence == null || state.masterSequence.director == null || !state.masterSequence.director.playableGraph.IsValid()) return; var masterPlayable = state.masterSequence.director.playableGraph.GetRootPlayable(0); if (!masterPlayable.Equals(Playable.Null) && masterPlayable.Equals(timelinePlayable) && timelinePlayable.GetGraph().IsMatchFrameRateEnabled()) timelinePlayable.SetTime(0); } #endif } }