using System; using System.Collections.Generic; using System.Linq; using UnityEditor.ShortcutManagement; using UnityEditor.Timeline.Actions; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; namespace UnityEditor.Timeline { [MenuEntry("Copy", MenuPriority.TimelineActionSection.copy)] [Shortcut("Main Menu/Edit/Copy", EventCommandNames.Copy)] class CopyAction : TimelineAction { public override ActionValidity Validate(ActionContext context) { if (SelectionManager.Count() == 0) return ActionValidity.NotApplicable; if (context.tracks.ContainsTimelineMarkerTrack(context.timeline)) return ActionValidity.NotApplicable; return ActionValidity.Valid; } public override bool Execute(ActionContext context) { TimelineEditor.clipboard.Clear(); var clips = context.clips; if (clips.Any()) { clips.Invoke(); } var markers = context.markers; if (markers.Any()) { markers.Invoke(); } var tracks = context.tracks; if (tracks.Any()) { CopyTracksToClipboard.Do(tracks.ToArray()); } return true; } } [MenuEntry("Paste", MenuPriority.TimelineActionSection.paste)] [Shortcut("Main Menu/Edit/Paste", EventCommandNames.Paste)] class PasteAction : TimelineAction { public override ActionValidity Validate(ActionContext context) { return CanPaste(context.invocationTime) ? ActionValidity.Valid : ActionValidity.Invalid; } public override bool Execute(ActionContext context) { if (!CanPaste(context.invocationTime)) return false; PasteItems(context.invocationTime); PasteTracks(); TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved); return true; } static bool CanPaste(double? invocationTime) { var copiedItems = TimelineEditor.clipboard.GetCopiedItems().ToList(); if (!copiedItems.Any()) return TimelineEditor.clipboard.GetTracks().Any(); return CanPasteItems(copiedItems, invocationTime); } static bool CanPasteItems(ICollection itemsGroups, double? invocationTime) { var hasItemsCopiedFromMultipleTracks = itemsGroups.Count > 1; var allItemsCopiedFromCurrentAsset = itemsGroups.All(x => x.targetTrack.timelineAsset == TimelineEditor.inspectedAsset); var hasUsedShortcut = invocationTime == null; var anySourceLocked = itemsGroups.Any(x => x.targetTrack != null && x.targetTrack.lockedInHierarchy); var targetTrack = GetPickedTrack(); if (targetTrack == null) targetTrack = SelectionManager.SelectedTracks().FirstOrDefault(); //do not paste if the user copied items from another timeline //if the copied items comes from > 1 track (since we do not know where to paste the copied items) //or if a keyboard shortcut was used (since the user will not see the paste result) if (!allItemsCopiedFromCurrentAsset) { var isSelectedTrackInCurrentAsset = targetTrack != null && targetTrack.timelineAsset == TimelineEditor.inspectedAsset; if (hasItemsCopiedFromMultipleTracks || (hasUsedShortcut && !isSelectedTrackInCurrentAsset)) return false; } // pasting to items to their source track, if items from multiple tracks are selected // and no track is in the selection items will each be pasted to their respective track. if (targetTrack == null || itemsGroups.All(x => x.targetTrack == targetTrack)) return !anySourceLocked; if (hasItemsCopiedFromMultipleTracks) { //do not paste if the track which received the paste action does not contain a copied clip return !anySourceLocked && itemsGroups.Select(x => x.targetTrack).Contains(targetTrack); } var copiedItems = itemsGroups.SelectMany(i => i.items); return IsTrackValidForItems(targetTrack, copiedItems); } static void PasteItems(double? invocationTime) { var copiedItems = TimelineEditor.clipboard.GetCopiedItems().ToList(); var numberOfUniqueParentsInClipboard = copiedItems.Count(); if (numberOfUniqueParentsInClipboard == 0) return; List newItems; //if the copied items were on a single parent, then use the mouse position to get the parent OR the original parent if (numberOfUniqueParentsInClipboard == 1) { var itemsGroup = copiedItems.First(); TrackAsset target = null; if (invocationTime.HasValue) target = GetPickedTrack(); if (target == null) target = FindSuitableParentForSingleTrackPasteWithoutMouse(itemsGroup); var candidateTime = invocationTime ?? TimelineHelpers.GetCandidateTime(null, target); newItems = TimelineHelpers.DuplicateItemsUsingCurrentEditMode(TimelineEditor.clipboard.exposedPropertyTable, TimelineEditor.inspectedDirector, itemsGroup, target, candidateTime, "Paste Items").ToList(); } //if copied items were on multiple parents, then the destination parents are the same as the original parents else { var time = invocationTime ?? TimelineHelpers.GetCandidateTime(null, copiedItems.Select(c => c.targetTrack).ToArray()); newItems = TimelineHelpers.DuplicateItemsUsingCurrentEditMode(TimelineEditor.clipboard.exposedPropertyTable, TimelineEditor.inspectedDirector, copiedItems, time, "Paste Items").ToList(); } TimelineHelpers.FrameItems(newItems); SelectionManager.RemoveTimelineSelection(); foreach (var item in newItems) { SelectionManager.Add(item); } } static TrackAsset FindSuitableParentForSingleTrackPasteWithoutMouse(ItemsPerTrack itemsGroup) { var groupParent = itemsGroup.targetTrack; //set a main parent in the clipboard var selectedTracks = SelectionManager.SelectedTracks(); if (selectedTracks.Contains(groupParent)) { return groupParent; } //find a selected track suitable for all items var itemsToPaste = itemsGroup.items; var compatibleTrack = selectedTracks.FirstOrDefault(t => IsTrackValidForItems(t, itemsToPaste)); return compatibleTrack != null ? compatibleTrack : groupParent; } static bool IsTrackValidForItems(TrackAsset track, IEnumerable items) { if (track == null || track.lockedInHierarchy) return false; return items.All(i => i.IsCompatibleWithTrack(track)); } static TrackAsset GetPickedTrack() { if (PickerUtils.pickedElements == null) return null; var rowGUI = PickerUtils.pickedElements.OfType().FirstOrDefault(); if (rowGUI != null) return rowGUI.asset; return null; } static void PasteTracks() { var trackData = TimelineEditor.clipboard.GetTracks().ToList(); if (trackData.Any()) { SelectionManager.RemoveTimelineSelection(); } foreach (var track in trackData) { var newTrack = track.item.Duplicate(TimelineEditor.clipboard.exposedPropertyTable, TimelineEditor.inspectedDirector, TimelineEditor.inspectedAsset); var newTracks = newTrack.GetFlattenedChildTracks().Append(newTrack); var bindingIt = track.bindings.GetEnumerator(); var newTrackIt = newTracks.GetEnumerator(); while (bindingIt.MoveNext() && newTrackIt.MoveNext()) { if (bindingIt.Current != null) BindingUtility.Bind(TimelineEditor.inspectedDirector, newTrackIt.Current, bindingIt.Current); } SelectionManager.Add(newTrack); foreach (var childTrack in newTrack.GetFlattenedChildTracks()) { SelectionManager.Add(childTrack); } if (track.parent != null && track.parent.timelineAsset == TimelineEditor.inspectedAsset) { TrackExtensions.ReparentTracks(new List { newTrack }, track.parent, track.item); } } } } [MenuEntry("Duplicate", MenuPriority.TimelineActionSection.duplicate)] [Shortcut("Main Menu/Edit/Duplicate", EventCommandNames.Duplicate)] class DuplicateAction : TimelineAction { public override ActionValidity Validate(ActionContext context) { IEnumerable tracks = context.tracks.RemoveTimelineMarkerTrackFromList(context.timeline); return context.clips.Any() || tracks.Any() || context.markers.Any() ? ActionValidity.Valid : ActionValidity.NotApplicable; } public bool Execute(Func gapBetweenItems) { return Execute(TimelineEditor.CurrentContext(), gapBetweenItems); } public override bool Execute(ActionContext context) { return Execute(context, (item1, item2) => ItemsUtils.TimeGapBetweenItems(item1, item2)); } internal bool Execute(ActionContext context, Func gapBetweenItems) { List items = new List(); items.AddRange(context.clips.Select(p => p.ToItem())); items.AddRange(context.markers.Select(p => p.ToItem())); List selectedItems = items.ToItemsPerTrack().ToList(); if (selectedItems.Any()) { var requestedTime = CalculateDuplicateTime(selectedItems, gapBetweenItems); var duplicatedItems = TimelineHelpers.DuplicateItemsUsingCurrentEditMode(TimelineEditor.inspectedDirector, TimelineEditor.inspectedDirector, selectedItems, requestedTime, "Duplicate Items"); TimelineHelpers.FrameItems(duplicatedItems); SelectionManager.RemoveTimelineSelection(); foreach (var item in duplicatedItems) SelectionManager.Add(item); } var tracks = context.tracks.ToArray(); if (tracks.Length > 0) tracks.Invoke(); TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved); return true; } static double CalculateDuplicateTime(IEnumerable duplicatedItems, Func gapBetweenItems) { //Find the end time of the rightmost item var itemsOnTracks = duplicatedItems.SelectMany(i => i.targetTrack.GetItems()).ToList(); var time = itemsOnTracks.Max(i => i.end); //From all the duplicated items, select the leftmost items var firstDuplicatedItems = duplicatedItems.Select(i => i.leftMostItem); var leftMostDuplicatedItems = firstDuplicatedItems.OrderBy(i => i.start).GroupBy(i => i.start).FirstOrDefault(); if (leftMostDuplicatedItems == null) return 0.0; foreach (var leftMostItem in leftMostDuplicatedItems) { var siblings = leftMostItem.parentTrack.GetItems(); var rightMostSiblings = siblings.OrderByDescending(i => i.end).GroupBy(i => i.end).FirstOrDefault(); if (rightMostSiblings == null) continue; foreach (var sibling in rightMostSiblings) time = Math.Max(time, sibling.end + gapBetweenItems(leftMostItem, sibling)); } return time; } } [MenuEntry("Delete", MenuPriority.TimelineActionSection.delete)] [Shortcut("Main Menu/Edit/Delete", EventCommandNames.Delete)] [ShortcutPlatformOverride(RuntimePlatform.OSXEditor, KeyCode.Backspace, ShortcutModifiers.Action)] [ActiveInMode(TimelineModes.Default)] class DeleteAction : TimelineAction { public override ActionValidity Validate(ActionContext context) { return CanDelete(context) ? ActionValidity.Valid : ActionValidity.Invalid; } static bool CanDelete(ActionContext context) { if (TimelineWindow.instance.state.editSequence.isReadOnly) return false; if (context.tracks.ContainsTimelineMarkerTrack(context.timeline)) return false; // All() returns true when empty return context.tracks.All(x => !x.lockedInHierarchy) && context.clips.All(x => x.GetParentTrack() == null || !x.GetParentTrack().lockedInHierarchy) && context.markers.All(x => x.parent == null || !x.parent.lockedInHierarchy); } public override bool Execute(ActionContext context) { if (!CanDelete(context)) return false; var selectedItems = context.clips.Select(p => p.ToItem()).ToList(); selectedItems.AddRange(context.markers.Select(p => p.ToItem())); DeleteItems(selectedItems); if (context.tracks.Any() && SelectionManager.GetCurrentInlineEditorCurve() == null) context.tracks.Invoke(); TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved); return selectedItems.Any() || context.tracks.Any(); } internal static void DeleteItems(IEnumerable items) { var tracks = items.GroupBy(c => c.parentTrack); foreach (var track in tracks) TimelineUndo.PushUndo(track.Key, L10n.Tr("Delete Items")); TimelineAnimationUtilities.UnlinkAnimationWindowFromClips(items.OfType().Select(i => i.clip)); EditMode.PrepareItemsDelete(ItemsUtils.ToItemsPerTrack(items)); EditModeUtils.Delete(items); SelectionManager.RemoveAllClips(); } } [MenuEntry("Match Content", MenuPriority.TimelineActionSection.matchContent)] [Shortcut(Shortcuts.Timeline.matchContent)] class MatchContent : TimelineAction { public override ActionValidity Validate(ActionContext actionContext) { var clips = actionContext.clips; if (!clips.Any()) return ActionValidity.NotApplicable; return clips.Any(TimelineHelpers.HasUsableAssetDuration) ? ActionValidity.Valid : ActionValidity.Invalid; } public override bool Execute(ActionContext actionContext) { var clips = actionContext.clips; return clips.Any() && ClipModifier.MatchContent(clips); } } [Shortcut(Shortcuts.Timeline.play)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class PlayTimelineAction : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { var currentState = TimelineEditor.state.playing; TimelineEditor.state.SetPlaying(!currentState); return true; } } [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class SelectAllAction : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { // otherwise select all tracks. SelectionManager.Clear(); TimelineWindow.instance.allTracks.ForEach(x => SelectionManager.Add(x.track)); return true; } } [Shortcut(Shortcuts.Timeline.previousFrame)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class PreviousFrameAction : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { if (TimelineEditor.inspectedAsset == null) return false; TimelineEditor.inspectedSequenceTime = TimeUtility.PreviousFrameTime(TimelineEditor.inspectedSequenceTime, TimelineEditor.inspectedAsset.editorSettings.frameRate); return true; } } [Shortcut(Shortcuts.Timeline.nextFrame)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class NextFrameAction : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { if (TimelineEditor.inspectedAsset == null) return false; TimelineEditor.inspectedSequenceTime = TimeUtility.NextFrameTime(TimelineEditor.inspectedSequenceTime, TimelineEditor.inspectedAsset.editorSettings.frameRate); return true; } } [Shortcut(Shortcuts.Timeline.frameAll)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class FrameAllAction : TimelineAction { public override ActionValidity Validate(ActionContext context) { if (context.timeline != null && !context.timeline.flattenedTracks.Any()) return ActionValidity.NotApplicable; return ActionValidity.Valid; } public override bool Execute(ActionContext actionContext) { var inlineCurveEditor = SelectionManager.GetCurrentInlineEditorCurve(); if (FrameSelectedAction.ShouldHandleInlineCurve(inlineCurveEditor)) { FrameSelectedAction.FrameInlineCurves(inlineCurveEditor, false); return true; } if (TimelineWindow.instance.state.IsCurrentEditingASequencerTextField()) return false; var visibleTracks = TimelineWindow.instance.treeView.visibleTracks.ToList(); if (TimelineEditor.inspectedAsset != null && TimelineEditor.inspectedAsset.markerTrack != null) visibleTracks.Add(TimelineEditor.inspectedAsset.markerTrack); if (visibleTracks.Count == 0) return false; var startTime = float.MaxValue; var endTime = float.MinValue; foreach (var t in visibleTracks) { if (t == null) continue; // time range based on track's curves and clips. double trackStart, trackEnd, trackDuration; t.GetSequenceTime(out trackStart, out trackDuration); trackEnd = trackStart + trackDuration; // take track's markers into account double itemsStart, itemsEnd; ItemsUtils.GetItemRange(t, out itemsStart, out itemsEnd); startTime = Mathf.Min(startTime, (float)trackStart, (float)itemsStart); endTime = Mathf.Max(endTime, (float)(trackEnd), (float)itemsEnd); } FrameSelectedAction.FrameRange(startTime, endTime); return true; } } [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class FrameSelectedAction : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public static void FrameRange(float startTime, float endTime) { if (startTime > endTime) { return; } var halfDuration = endTime - Math.Max(0.0f, startTime); if (halfDuration > 0.0f) { TimelineEditor.visibleTimeRange = new Vector2(Mathf.Max(0.0f, startTime - (halfDuration * 0.1f)), endTime + (halfDuration * 0.1f)); } else { // start == end // keep the zoom level constant, only pan the time area to center the item var currentRange = TimelineEditor.visibleTimeRange.y - TimelineEditor.visibleTimeRange.x; TimelineEditor.visibleTimeRange = new Vector2(startTime - currentRange / 2, startTime + currentRange / 2); } TimelineZoomManipulator.InvalidateWheelZoom(); TimelineEditor.Refresh(RefreshReason.SceneNeedsUpdate); } public override bool Execute(ActionContext actionContext) { var inlineCurveEditor = SelectionManager.GetCurrentInlineEditorCurve(); if (ShouldHandleInlineCurve(inlineCurveEditor)) { FrameInlineCurves(inlineCurveEditor, true); return true; } if (TimelineWindow.instance.state.IsCurrentEditingASequencerTextField()) return false; if (SelectionManager.Count() == 0) { actionContext.Invoke(); return true; } var startTime = float.MaxValue; var endTime = float.MinValue; var clips = actionContext.clips.Select(ItemToItemGui.GetGuiForClip); var markers = actionContext.markers; if (!clips.Any() && !markers.Any()) return false; foreach (var c in clips) { startTime = Mathf.Min(startTime, (float)c.clip.start); endTime = Mathf.Max(endTime, (float)c.clip.end); if (c.clipCurveEditor != null) { c.clipCurveEditor.FrameClip(); } } foreach (var marker in markers) { startTime = Mathf.Min(startTime, (float)marker.time); endTime = Mathf.Max(endTime, (float)marker.time); } FrameRange(startTime, endTime); return true; } public static bool ShouldHandleInlineCurve(IClipCurveEditorOwner curveEditorOwner) { return curveEditorOwner?.clipCurveEditor != null && curveEditorOwner.inlineCurvesSelected && curveEditorOwner.owner != null && curveEditorOwner.owner.GetShowInlineCurves(); } public static void FrameInlineCurves(IClipCurveEditorOwner curveEditorOwner, bool selectionOnly) { var curveEditor = curveEditorOwner.clipCurveEditor.curveEditor; var frameBounds = selectionOnly ? curveEditor.GetSelectionBounds() : curveEditor.GetClipBounds(); var clipGUI = curveEditorOwner as TimelineClipGUI; var areaOffset = 0.0f; if (clipGUI != null) { areaOffset = (float)Math.Max(0.0, clipGUI.clip.FromLocalTimeUnbound(0.0)); var timeScale = (float)clipGUI.clip.timeScale; // Note: The getter for clip.timeScale is guaranteed to never be zero. // Apply scaling var newMin = frameBounds.min.x / timeScale; var newMax = (frameBounds.max.x - frameBounds.min.x) / timeScale + newMin; frameBounds.SetMinMax( new Vector3(newMin, frameBounds.min.y, frameBounds.min.z), new Vector3(newMax, frameBounds.max.y, frameBounds.max.z)); } curveEditor.Frame(frameBounds, true, true); var area = curveEditor.shownAreaInsideMargins; area.x += areaOffset; var curveStart = curveEditorOwner.clipCurveEditor.dataSource.start; FrameRange(curveStart + frameBounds.min.x, curveStart + frameBounds.max.x); } } [Shortcut(Shortcuts.Timeline.previousKey)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class PrevKeyAction : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { if (TimelineEditor.inspectedAsset == null) return false; var keyTraverser = new Utilities.KeyTraverser(TimelineEditor.inspectedAsset, 0.01f / (float)TimelineEditor.inspectedAsset.editorSettings.frameRate); var time = keyTraverser.GetPrevKey((float)TimelineEditor.inspectedSequenceTime, TimelineWindow.instance.state.dirtyStamp); if (time != TimelineEditor.inspectedSequenceTime) { TimelineEditor.inspectedSequenceTime = time; } return true; } } [Shortcut(Shortcuts.Timeline.nextKey)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class NextKeyAction : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { if (TimelineEditor.inspectedAsset == null) return false; var keyTraverser = new Utilities.KeyTraverser(TimelineEditor.inspectedAsset, 0.01f / (float)TimelineEditor.inspectedAsset.editorSettings.frameRate); var time = keyTraverser.GetNextKey((float)TimelineEditor.inspectedSequenceTime, TimelineWindow.instance.state.dirtyStamp); if (time != TimelineEditor.inspectedSequenceTime) { TimelineEditor.inspectedSequenceTime = time; } return true; } } [Shortcut(Shortcuts.Timeline.goToStart)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class GotoStartAction : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { TimelineEditor.inspectedSequenceTime = 0.0f; TimelineWindow.instance.state.EnsurePlayHeadIsVisible(); return true; } } [Shortcut(Shortcuts.Timeline.goToEnd)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class GotoEndAction : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { TimelineEditor.inspectedSequenceTime = TimelineWindow.instance.state.editSequence.duration; TimelineWindow.instance.state.EnsurePlayHeadIsVisible(); return true; } } [Shortcut(Shortcuts.Timeline.zoomIn)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class ZoomIn : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { TimelineZoomManipulator.Instance.DoZoom(1.15f); return true; } } [Shortcut(Shortcuts.Timeline.zoomOut)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class ZoomOut : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { TimelineZoomManipulator.Instance.DoZoom(0.85f); return true; } } [Shortcut(Shortcuts.Timeline.navigateLeft)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class NavigateLeft : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.NavigateLeft(actionContext.tracks); } } [Shortcut(Shortcuts.Timeline.navigateRight)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class NavigateRight : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.NavigateRight(actionContext.tracks); } } [Shortcut(Shortcuts.Timeline.toggleCollapseTrack)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class ToggleCollapseGroup : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.ToggleCollapseGroup(actionContext.tracks); } } [Shortcut(Shortcuts.Timeline.selectLeftItem)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class SelectLeftClip : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { // Switches to track header if no left track exists return KeyboardNavigation.SelectLeftItem(); } } [Shortcut(Shortcuts.Timeline.selectRightItem)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class SelectRightClip : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.SelectRightItem(); } } [Shortcut(Shortcuts.Timeline.selectUpItem)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class SelectUpClip : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.SelectUpItem(); } } [Shortcut(Shortcuts.Timeline.selectUpTrack)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class SelectUpTrack : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.SelectUpTrack(); } } [Shortcut(Shortcuts.Timeline.selectDownItem)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class SelectDownClip : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.SelectDownItem(); } } [Shortcut(Shortcuts.Timeline.selectDownTrack)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class SelectDownTrack : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { if (!KeyboardNavigation.ClipAreaActive() && !KeyboardNavigation.TrackHeadActive()) return KeyboardNavigation.FocusFirstVisibleItem(); else return KeyboardNavigation.SelectDownTrack(); } } [Shortcut(Shortcuts.Timeline.multiSelectLeft)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class MultiselectLeftClip : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.SelectLeftItem(true); } } [Shortcut(Shortcuts.Timeline.multiSelectRight)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class MultiselectRightClip : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.SelectRightItem(true); } } [Shortcut(Shortcuts.Timeline.multiSelectUp)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class MultiselectUpTrack : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.SelectUpTrack(true); } } [Shortcut(Shortcuts.Timeline.multiSelectDown)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class MultiselectDownTrack : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { return KeyboardNavigation.SelectDownTrack(true); } } [Shortcut(Shortcuts.Timeline.toggleClipTrackArea)] [ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)] class ToggleClipTrackArea : TimelineAction { public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid; public override bool Execute(ActionContext actionContext) { if (KeyboardNavigation.TrackHeadActive()) return KeyboardNavigation.FocusFirstVisibleItem(actionContext.tracks); if (!KeyboardNavigation.ClipAreaActive()) return KeyboardNavigation.FocusFirstVisibleItem(); var item = KeyboardNavigation.GetVisibleSelectedItems().LastOrDefault(); if (item != null) SelectionManager.SelectOnly(item.parentTrack); return true; } } [MenuEntry("Key All Animated", MenuPriority.TimelineActionSection.keyAllAnimated)] [Shortcut(Shortcuts.Timeline.keyAllAnimated)] class KeyAllAnimated : TimelineAction { public override ActionValidity Validate(ActionContext actionContext) { return CanExecute(TimelineEditor.state, actionContext) ? ActionValidity.Valid : ActionValidity.NotApplicable; } public override bool Execute(ActionContext actionContext) { WindowState state = TimelineEditor.state; PlayableDirector director = TimelineEditor.inspectedDirector; if (!CanExecute(state, actionContext) || director == null) return false; IEnumerable keyableTracks = GetKeyableTracks(state, actionContext); var curveSelected = SelectionManager.GetCurrentInlineEditorCurve(); if (curveSelected != null) { var sel = curveSelected.clipCurveEditor.GetSelectedProperties().ToList(); var go = (director.GetGenericBinding(curveSelected.owner) as Component).gameObject; if (sel.Count > 0) { TimelineRecording.KeyProperties(go, state, sel); } else { var binding = director.GetGenericBinding(curveSelected.owner) as Component; TimelineRecording.KeyAllProperties(binding, state); } } else { foreach (var track in keyableTracks) { var binding = director.GetGenericBinding(track) as Component; TimelineRecording.KeyAllProperties(binding, state); } } return true; } static IEnumerable GetKeyableTracks(WindowState state, ActionContext context) { if (!context.clips.Any() && !context.tracks.Any()) //no selection -> animate all recorded tracks return context.timeline.flattenedTracks.Where(state.IsArmedForRecord); List parentTracks = context.tracks.ToList(); parentTracks.AddRange(context.clips.Select(clip => clip.GetParentTrack()).Distinct()); if (!parentTracks.All(state.IsArmedForRecord)) return Enumerable.Empty(); return parentTracks; } static bool CanExecute(WindowState state, ActionContext context) { if (context.timeline == null) return false; if (context.markers.Any()) return false; if (context.tracks.ContainsTimelineMarkerTrack(context.timeline)) return false; IClipCurveEditorOwner curveSelected = SelectionManager.GetCurrentInlineEditorCurve(); // Can't have an inline curve selected and have multiple tracks also. if (curveSelected != null) { return state.IsArmedForRecord(curveSelected.owner); } return GetKeyableTracks(state, context).Any(); } } }