using System.Collections.Generic; using System.ComponentModel; using System.Linq; using JetBrains.Annotations; using UnityEngine; using UnityEngine.Timeline; namespace UnityEditor.Timeline { [ActiveInMode(TimelineModes.Default)] abstract class TrackAction : MenuItemActionBase { public abstract bool Execute(WindowState state, TrackAsset[] tracks); protected virtual MenuActionDisplayState GetDisplayState(WindowState state, TrackAsset[] tracks) { return tracks.Length > 0 ? MenuActionDisplayState.Visible : MenuActionDisplayState.Disabled; } protected virtual bool IsChecked(WindowState state, TrackAsset[] tracks) { return false; } protected virtual string GetDisplayName(TrackAsset[] tracks) { return menuName; } public static void Invoke(WindowState state, TrackAsset[] tracks) where T : TrackAction { actions.First(x => x.GetType() == typeof(T)).Execute(state, tracks); } static List s_ActionClasses; static List actions { get { if (s_ActionClasses == null) s_ActionClasses = GetActionsOfType(typeof(TrackAction)) .Select(x => (TrackAction)x.GetConstructors()[0].Invoke(null)) .OrderBy(x => x.priority).ThenBy(x => x.category) .ToList(); return s_ActionClasses; } } public static void GetMenuEntries(WindowState state, Vector2? mousePos, TrackAsset[] tracks, List items) { var mode = TimelineWindow.instance.currentMode.mode; foreach (var action in actions) { if (!action.showInMenu) continue; var actionItem = action; items.Add( new MenuActionItem() { category = action.category, entryName = action.GetDisplayName(tracks), shortCut = action.shortCut, isChecked = action.IsChecked(state, tracks), isActiveInMode = IsActionActiveInMode(action, mode), priority = action.priority, state = action.GetDisplayState(state, tracks), callback = () => { actionItem.mousePosition = mousePos; actionItem.Execute(state, tracks); actionItem.mousePosition = null; } } ); } } public static bool HandleShortcut(WindowState state, Event evt, TrackAsset[] tracks) { foreach (var action in actions) { var attr = action.GetType().GetCustomAttributes(typeof(ShortcutAttribute), true); foreach (ShortcutAttribute shortcut in attr) { if (shortcut.MatchesEvent(evt)) { if (s_ShowActionTriggeredByShortcut) Debug.Log(action.GetType().Name); if (!IsActionActiveInMode(action, TimelineWindow.instance.currentMode.mode)) return false; return action.Execute(state, tracks); } } } return false; } // For testing internal MenuActionDisplayState InternalGetDisplayState(WindowState state, TrackAsset[] tracks) { return GetDisplayState(state, tracks); } } [MenuEntry("Edit in Animation Window", MenuOrder.TrackAction.EditInAnimationWindow)] class EditTrackInAnimationWindow : TrackAction { public static bool Do(WindowState state, TrackAsset track) { AnimationClip clipToEdit = null; AnimationTrack animationTrack = track as AnimationTrack; if (animationTrack != null) { if (!animationTrack.CanConvertToClipMode()) return false; clipToEdit = animationTrack.infiniteClip; } else if (track.hasCurves) { clipToEdit = track.curves; } if (clipToEdit == null) return false; var gameObject = state.GetSceneReference(track); var timeController = TimelineAnimationUtilities.CreateTimeController(state, CreateTimeControlClipData(track)); TimelineAnimationUtilities.EditAnimationClipWithTimeController(clipToEdit, timeController, gameObject); return true; } protected override MenuActionDisplayState GetDisplayState(WindowState state, TrackAsset[] tracks) { if (tracks.Length == 0) return MenuActionDisplayState.Hidden; if (tracks[0] is AnimationTrack) { var animTrack = tracks[0] as AnimationTrack; if (animTrack.CanConvertToClipMode()) return MenuActionDisplayState.Visible; } else if (tracks[0].hasCurves) { return MenuActionDisplayState.Visible; } return MenuActionDisplayState.Hidden; } public override bool Execute(WindowState state, TrackAsset[] tracks) { return Do(state, tracks[0]); } static TimelineWindowTimeControl.ClipData CreateTimeControlClipData(TrackAsset track) { var data = new TimelineWindowTimeControl.ClipData(); data.track = track; data.start = track.start; data.duration = track.duration; return data; } } [MenuEntry("Lock selected track only", MenuOrder.TrackAction.LockSelected)] class LockSelectedTrack : TrackAction { public static readonly string LockSelectedTrackOnlyText = L10n.Tr("Lock selected track only"); public static readonly string UnlockSelectedTrackOnlyText = L10n.Tr("Unlock selected track only"); protected override MenuActionDisplayState GetDisplayState(WindowState state, TrackAsset[] tracks) { if (tracks.Any(track => TimelineUtility.IsLockedFromGroup(track) || track is GroupTrack || !track.subTracksObjects.Any())) return MenuActionDisplayState.Hidden; return MenuActionDisplayState.Visible; } public override bool Execute(WindowState state, TrackAsset[] tracks) { if (!tracks.Any()) return false; var hasUnlockedTracks = tracks.Any(x => !x.locked); Lock(state, tracks.Where(p => !(p is GroupTrack)).ToArray(), hasUnlockedTracks); return true; } protected override string GetDisplayName(TrackAsset[] tracks) { return tracks.All(t => t.locked) ? UnlockSelectedTrackOnlyText : LockSelectedTrackOnlyText; } public static void Lock(WindowState state, TrackAsset[] tracks, bool shouldlock) { if (tracks.Length == 0) return; foreach (var track in tracks.Where(t => !TimelineUtility.IsLockedFromGroup(t))) { TimelineUndo.PushUndo(track, "Lock Tracks"); track.locked = shouldlock; } TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw); } } [MenuEntry("Lock", MenuOrder.TrackAction.LockTrack)] [Shortcut(Shortcuts.Timeline.toggleLock)] class LockTrack : TrackAction { public static readonly string UnlockText = L10n.Tr("Unlock"); protected override MenuActionDisplayState GetDisplayState(WindowState state, TrackAsset[] tracks) { bool hasUnlockableTracks = tracks.Any(x => TimelineUtility.IsLockedFromGroup(x)); if (hasUnlockableTracks) return MenuActionDisplayState.Disabled; return MenuActionDisplayState.Visible; } protected override string GetDisplayName(TrackAsset[] tracks) { return tracks.Any(x => !x.locked) ? base.GetDisplayName(tracks) : UnlockText; } public override bool Execute(WindowState state, TrackAsset[] tracks) { if (!tracks.Any()) return false; var hasUnlockedTracks = tracks.Any(x => !x.locked); SetLockState(tracks, hasUnlockedTracks, state); return true; } public static void SetLockState(TrackAsset[] tracks, bool shouldLock, WindowState state = null) { if (tracks.Length == 0) return; foreach (var track in tracks) { if (TimelineUtility.IsLockedFromGroup(track)) continue; if (track as GroupTrack == null) SetLockState(track.GetChildTracks().ToArray(), shouldLock, state); TimelineUndo.PushUndo(track, "Lock Tracks"); track.locked = shouldLock; } if (state != null) { // find the tracks we've locked. unselect anything locked and remove recording. foreach (var track in tracks) { if (TimelineUtility.IsLockedFromGroup(track) || !track.locked) continue; var flattenedChildTracks = track.GetFlattenedChildTracks(); foreach (var i in track.clips) SelectionManager.Remove(i); state.UnarmForRecord(track); foreach (var child in flattenedChildTracks) { SelectionManager.Remove(child); state.UnarmForRecord(child); foreach (var clip in child.GetClips()) SelectionManager.Remove(clip); } } // no need to rebuild, just repaint (including inspectors) InspectorWindow.RepaintAllInspectors(); state.editorWindow.Repaint(); } } } [UsedImplicitly] [MenuEntry("Show Markers", MenuOrder.TrackAction.ShowHideMarkers)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class ShowHideMarkers : TrackAction { protected override bool IsChecked(WindowState state, TrackAsset[] tracks) { return tracks.All(x => x.GetShowMarkers()); } protected override MenuActionDisplayState GetDisplayState(WindowState state, TrackAsset[] tracks) { if (tracks.Any(x => x is GroupTrack) || tracks.Any(t => t.GetMarkerCount() == 0)) return MenuActionDisplayState.Hidden; if (tracks.Any(t => t.lockedInHierarchy)) return MenuActionDisplayState.Disabled; return MenuActionDisplayState.Visible; } public override bool Execute(WindowState state, TrackAsset[] tracks) { if (!tracks.Any()) return false; var hasUnlockedTracks = tracks.Any(x => !x.GetShowMarkers()); ShowHide(state, tracks, hasUnlockedTracks); return true; } static void ShowHide(WindowState state, TrackAsset[] tracks, bool shouldLock) { if (tracks.Length == 0) return; var window = state.GetWindow(); foreach (var track in tracks) { window.SetShowTrackMarkers(track, shouldLock); } TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw); } } [MenuEntry("Mute selected track only", MenuOrder.TrackAction.MuteSelected), UsedImplicitly] class MuteSelectedTrack : TrackAction { public static readonly string UnmuteSelectedText = L10n.Tr("Unmute selected track only"); protected override MenuActionDisplayState GetDisplayState(WindowState state, TrackAsset[] tracks) { if (tracks.Any(track => TimelineUtility.IsParentMuted(track) || track is GroupTrack || !track.subTracksObjects.Any())) return MenuActionDisplayState.Hidden; return MenuActionDisplayState.Visible; } public override bool Execute(WindowState state, TrackAsset[] tracks) { if (!tracks.Any()) return false; var hasUnmutedTracks = tracks.Any(x => !x.muted); Mute(state, tracks.Where(p => !(p is GroupTrack)).ToArray(), hasUnmutedTracks); return true; } protected override string GetDisplayName(TrackAsset[] tracks) { return tracks.All(t => t.muted) ? UnmuteSelectedText : base.GetDisplayName(tracks); } public static void Mute(WindowState state, TrackAsset[] tracks, bool shouldMute) { if (tracks.Length == 0) return; foreach (var track in tracks.Where(t => !TimelineUtility.IsParentMuted(t))) { TimelineUndo.PushUndo(track, "Mute Tracks"); track.muted = shouldMute; } state.Refresh(); } } [MenuEntry("Mute", MenuOrder.TrackAction.MuteTrack)] [Shortcut(Shortcuts.Timeline.toggleMute)] class MuteTrack : TrackAction { public static readonly string UnMuteText = L10n.Tr("Unmute"); protected override MenuActionDisplayState GetDisplayState(WindowState state, TrackAsset[] tracks) { if (tracks.Any(track => TimelineUtility.IsParentMuted(track))) return MenuActionDisplayState.Disabled; return MenuActionDisplayState.Visible; } protected override string GetDisplayName(TrackAsset[] tracks) { return tracks.Any(x => !x.muted) ? base.GetDisplayName(tracks) : UnMuteText; } public override bool Execute(WindowState state, TrackAsset[] tracks) { if (!tracks.Any() || tracks.Any(track => TimelineUtility.IsParentMuted(track))) return false; var hasUnmutedTracks = tracks.Any(x => !x.muted); Mute(state, tracks, hasUnmutedTracks); return true; } public static void Mute(WindowState state, TrackAsset[] tracks, bool shouldMute) { if (tracks.Length == 0) return; foreach (var track in tracks) { if (track as GroupTrack == null) Mute(state, track.GetChildTracks().ToArray(), shouldMute); TimelineUndo.PushUndo(track, "Mute Tracks"); track.muted = shouldMute; } state.Refresh(); } } class DeleteTracks : TrackAction { public static void Do(TimelineAsset timeline, TrackAsset track) { SelectionManager.Remove(track); TrackModifier.DeleteTrack(timeline, track); } public override bool Execute(WindowState state, TrackAsset[] tracks) { // disable preview mode so deleted tracks revert to default state // Case 956129: Disable preview mode _before_ deleting the tracks, since clip data is still needed state.previewMode = false; TimelineAnimationUtilities.UnlinkAnimationWindowFromTracks(tracks); foreach (var track in tracks) Do(state.editSequence.asset, track); state.Refresh(); return true; } } class CopyTracksToClipboard : TrackAction { public static bool Do(WindowState state, TrackAsset[] tracks) { var action = new CopyTracksToClipboard(); return action.Execute(state, tracks); } public override bool Execute(WindowState state, TrackAsset[] tracks) { TimelineEditor.clipboard.CopyTracks(tracks); return true; } } class DuplicateTracks : TrackAction { public override bool Execute(WindowState state, TrackAsset[] tracks) { if (tracks.Any()) { SelectionManager.RemoveTimelineSelection(); } foreach (var track in TrackExtensions.FilterTracks(tracks)) { var newTrack = track.Duplicate(TimelineEditor.inspectedDirector, TimelineEditor.inspectedDirector); SelectionManager.Add(newTrack); foreach (var childTrack in newTrack.GetFlattenedChildTracks()) { SelectionManager.Add(childTrack); } } state.Refresh(); return true; } } [MenuEntry("Remove Invalid Markers", MenuOrder.TrackAction.RemoveInvalidMarkers), UsedImplicitly] class RemoveInvalidMarkersAction : TrackAction { protected override MenuActionDisplayState GetDisplayState(WindowState state, TrackAsset[] tracks) { if (tracks.Any(target => target != null && target.GetMarkerCount() != target.GetMarkersRaw().Count())) return MenuActionDisplayState.Visible; return MenuActionDisplayState.Hidden; } public override bool Execute(WindowState state, TrackAsset[] tracks) { bool anyRemoved = false; foreach (var target in tracks) { var invalids = target.GetMarkersRaw().Where(x => !(x is IMarker)).ToList(); foreach (var m in invalids) { anyRemoved = true; target.DeleteMarkerRaw(m); } } if (anyRemoved) TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved); return anyRemoved; } } }