diff --git a/Source/Editor/GUI/Timeline/Media.cs b/Source/Editor/GUI/Timeline/Media.cs index f902345fa..0f0025f76 100644 --- a/Source/Editor/GUI/Timeline/Media.cs +++ b/Source/Editor/GUI/Timeline/Media.cs @@ -215,7 +215,7 @@ namespace FlaxEditor.GUI.Timeline /// /// Gets a value indicating whether this media can be resized (duration changed). /// - public bool CanResize; + public bool CanResize = true; /// /// Initializes a new instance of the class. @@ -347,8 +347,9 @@ namespace FlaxEditor.GUI.Timeline var isMovingWholeMedia = _isMoving && !_startMoveRightEdge && !_startMoveLeftEdge; var borderHighlightColor = style.BorderHighlighted; var moveColor = style.ProgressNormal; + var selectedColor = style.BackgroundSelected; var moveThickness = 2.0f; - var borderColor = isMovingWholeMedia ? moveColor : (IsMouseOver ? borderHighlightColor : style.BorderNormal); + var borderColor = isMovingWholeMedia ? moveColor : (Timeline.SelectedMedia.Contains(this) ? selectedColor : (IsMouseOver ? borderHighlightColor : style.BorderNormal)); Render2D.DrawRectangle(bounds, borderColor, isMovingWholeMedia ? moveThickness : 1.0f); if (_startMoveLeftEdge) { @@ -384,9 +385,25 @@ namespace FlaxEditor.GUI.Timeline _startMoveDuration = DurationFrames; _startMoveLeftEdge = MoveLeftEdgeRect.Contains(ref location) && CanResize; _startMoveRightEdge = MoveRightEdgeRect.Contains(ref location) && CanResize; - StartMouseCapture(true); + if (_startMoveLeftEdge || _startMoveRightEdge) + return true; + if (Root.GetKey(KeyboardKeys.Control)) + { + // Add/Remove selection + if (_timeline.SelectedMedia.Contains(this)) + _timeline.Deselect(this); + else + _timeline.Select(this, true); + } + else + { + // Select (additive for the move) + _timeline.Select(this, true); + } + + _timeline.OnKeyframesMove(null, this, location, true, false); return true; } @@ -417,7 +434,8 @@ namespace FlaxEditor.GUI.Timeline } else { - StartFrame = _startMoveStartFrame + moveDelta; + // Move with global timeline selection + _timeline.OnKeyframesMove(null, this, location, false, false); } if (StartFrame != startFrame || DurationFrames != durationFrames) @@ -436,6 +454,15 @@ namespace FlaxEditor.GUI.Timeline { if (button == MouseButton.Left && _isMoving) { + if (!_startMoveLeftEdge && !_startMoveRightEdge && !Root.GetKey(KeyboardKeys.Control)) + { + var moveLocationDelta = Root.MousePosition - _startMoveLocation; + if (moveLocationDelta.Length < 4.0f) + { + // No move so just select itself + _timeline.Select(this); + } + } EndMoving(); return true; } @@ -501,19 +528,27 @@ namespace FlaxEditor.GUI.Timeline _startMoveLeftEdge = false; _startMoveRightEdge = false; - // Re-assign the media start/duration inside the undo recording block - if (_startMoveStartFrame != _startFrame || _startMoveDuration != _durationFrames) + if (_startMoveLeftEdge || _startMoveRightEdge) { - var endMoveStartFrame = _startFrame; - var endMoveDuration = _durationFrames; - _startFrame = _startMoveStartFrame; - _durationFrames = _startMoveDuration; - using (new TrackUndoBlock(_tack)) + // Re-assign the media start/duration inside the undo recording block + if (_startMoveStartFrame != _startFrame || _startMoveDuration != _durationFrames) { - _startFrame = endMoveStartFrame; - _durationFrames = endMoveDuration; + var endMoveStartFrame = _startFrame; + var endMoveDuration = _durationFrames; + _startFrame = _startMoveStartFrame; + _durationFrames = _startMoveDuration; + using (new TrackUndoBlock(_tack)) + { + _startFrame = endMoveStartFrame; + _durationFrames = endMoveDuration; + } } } + else + { + // Global timeline selection moving end + _timeline.OnKeyframesMove(null, this, _mouseLocation, false, true); + } EndMouseCapture(); } diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index e20ae0a69..a89df1fc2 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -219,6 +219,10 @@ namespace FlaxEditor.GUI.Timeline private bool _isRightMouseButtonDown; private Vector2 _rightMouseButtonDownPos; private Vector2 _rightMouseButtonMovePos; + private Vector2 _mediaMoveStartPos; + private int[] _mediaMoveStartFrames; + private List _mediaMoveStartTracks; + private byte[][] _mediaMoveStartData; private float _zoom = 1.0f; private bool _isMovingPositionHandle; private bool _canPlayPause = true, _canStop = true; @@ -1375,7 +1379,7 @@ namespace FlaxEditor.GUI.Timeline if (!addToSelection) { SelectedTracks.Clear(); - SelectedMedia.Clear(); + OnKeyframesDeselect(null); } SelectedMedia.Add(media); OnSelectionChanged(); @@ -1425,47 +1429,40 @@ namespace FlaxEditor.GUI.Timeline } /// - /// Deletes the selected tracks/media events. + /// Deletes the selected tracks. /// /// True if use undo/redo action for track removing. - public void DeleteSelection(bool withUndo = true) + public void DeleteSelectedTracks(bool withUndo = true) { - if (SelectedMedia.Count > 0) + if (SelectedTracks.Count == 0) + return; + var tracks = new List(SelectedTracks.Count); + for (int i = 0; i < SelectedTracks.Count; i++) { - throw new NotImplementedException("TODO: removing selected media events"); + GetTracks(SelectedTracks[i], tracks); } - - if (SelectedTracks.Count > 0) + SelectedTracks.Clear(); + if (withUndo && Undo != null && Undo.Enabled) { - // Delete selected tracks - var tracks = new List(SelectedTracks.Count); - for (int i = 0; i < SelectedTracks.Count; i++) + if (tracks.Count == 1) { - GetTracks(SelectedTracks[i], tracks); + Undo.AddAction(new AddRemoveTrackAction(this, tracks[0], false)); } - SelectedTracks.Clear(); - if (withUndo && Undo != null && Undo.Enabled) + else { - if (tracks.Count == 1) - { - Undo.AddAction(new AddRemoveTrackAction(this, tracks[0], false)); - } - else - { - var actions = new List(); - for (int i = tracks.Count - 1; i >= 0; i--) - actions.Add(new AddRemoveTrackAction(this, tracks[i], false)); - Undo.AddAction(new MultiUndoAction(actions, "Remove tracks")); - } + var actions = new List(); + for (int i = tracks.Count - 1; i >= 0; i--) + actions.Add(new AddRemoveTrackAction(this, tracks[i], false)); + Undo.AddAction(new MultiUndoAction(actions, "Remove tracks")); } - for (int i = tracks.Count - 1; i >= 0; i--) - { - tracks[i].ParentTrack = null; - OnDeleteTrack(tracks[i]); - } - OnTracksChanged(); - MarkAsEdited(); } + for (int i = tracks.Count - 1; i >= 0; i--) + { + tracks[i].ParentTrack = null; + OnDeleteTrack(tracks[i]); + } + OnTracksChanged(); + MarkAsEdited(); } /// @@ -1539,6 +1536,32 @@ namespace FlaxEditor.GUI.Timeline MarkAsEdited(); } + /// + /// Adds the media. + /// + /// The track to add media to. + /// The media to add. + /// True if use undo/redo action for media adding. + public void AddMedia(Track track, Media media, bool withUndo = true) + { + if (track == null || media == null) + throw new ArgumentNullException(); + if (media.Track != null) + throw new InvalidOperationException(); + if (withUndo && Undo != null && Undo.Enabled) + { + var before = EditTrackAction.CaptureData(track); + track.AddMedia(media); + var after = EditTrackAction.CaptureData(track); + Undo.AddAction(new EditTrackAction(this, track, before, after)); + } + else + { + track.AddMedia(media); + } + MarkAsEdited(); + } + /// /// Called to delete media. /// @@ -1551,89 +1574,82 @@ namespace FlaxEditor.GUI.Timeline } /// - /// Duplicates the selected tracks/media events. + /// Duplicates the selected tracks. /// /// True if use undo/redo action for track duplication. - public void DuplicateSelection(bool withUndo = true) + public void DuplicateSelectedTracks(bool withUndo = true) { - if (SelectedMedia.Count > 0) + if (SelectedTracks.Count == 0) + return; + var tracks = new List(SelectedTracks.Count); + for (int i = 0; i < SelectedTracks.Count; i++) { - throw new NotImplementedException("TODO: duplicating selected media events"); + GetTracks(SelectedTracks[i], tracks); } - - if (SelectedTracks.Count > 0) + var clones = new Track[tracks.Count]; + for (int i = 0; i < tracks.Count; i++) { - // Duplicate selected tracks - var tracks = new List(SelectedTracks.Count); - for (int i = 0; i < SelectedTracks.Count; i++) + var track = tracks[i]; + var options = new TrackCreateOptions { - GetTracks(SelectedTracks[i], tracks); + Archetype = track.Archetype, + Flags = track.Flags, + }; + var clone = options.Archetype.Create(options); + clone.Name = track.CanRename ? GetValidTrackName(track.Name) : track.Name; + clone.Color = track.Color; + clone.IsExpanded = track.IsExpanded; + byte[] data; + using (var memory = new MemoryStream(512)) + using (var stream = new BinaryWriter(memory)) + { + // TODO: reuse memory stream to improve tracks duplication performance + options.Archetype.Save(track, stream); + data = memory.ToArray(); } - var clones = new Track[tracks.Count]; - for (int i = 0; i < tracks.Count; i++) + using (var memory = new MemoryStream(data)) + using (var stream = new BinaryReader(memory)) { - var track = tracks[i]; - var options = new TrackCreateOptions + track.Archetype.Load(Timeline.FormatVersion, clone, stream); + } + var trackParent = track.ParentTrack; + var trackIndex = track.TrackIndex + 1; + if (trackParent != null && tracks.Contains(trackParent)) + { + for (int j = 0; j < i; j++) { - Archetype = track.Archetype, - Flags = track.Flags, - }; - var clone = options.Archetype.Create(options); - clone.Name = track.CanRename ? GetValidTrackName(track.Name) : track.Name; - clone.Color = track.Color; - clone.IsExpanded = track.IsExpanded; - byte[] data; - using (var memory = new MemoryStream(512)) - using (var stream = new BinaryWriter(memory)) - { - // TODO: reuse memory stream to improve tracks duplication performance - options.Archetype.Save(track, stream); - data = memory.ToArray(); - } - using (var memory = new MemoryStream(data)) - using (var stream = new BinaryReader(memory)) - { - track.Archetype.Load(Timeline.FormatVersion, clone, stream); - } - var trackParent = track.ParentTrack; - var trackIndex = track.TrackIndex + 1; - if (trackParent != null && tracks.Contains(trackParent)) - { - for (int j = 0; j < i; j++) + if (tracks[j] == trackParent) { - if (tracks[j] == trackParent) - { - trackParent = clones[j]; - break; - } + trackParent = clones[j]; + break; } - trackIndex--; } - clone.ParentTrack = trackParent; - clone.TrackIndex = trackIndex; - track.OnDuplicated(clone); - AddTrack(clone, false); - clones[i] = clone; + trackIndex--; } - OnTracksOrderChanged(); - if (withUndo && Undo != null && Undo.Enabled) - { - if (clones.Length == 1) - { - Undo.AddAction(new AddRemoveTrackAction(this, clones[0], true)); - } - else - { - var actions = new List(); - for (int i = 0; i < clones.Length; i++) - actions.Add(new AddRemoveTrackAction(this, clones[i], true)); - Undo.AddAction(new MultiUndoAction(actions, "Remove tracks")); - } - } - OnTracksChanged(); - MarkAsEdited(); - SelectedTracks[0].Focus(); + clone.ParentTrack = trackParent; + clone.TrackIndex = trackIndex; + track.OnDuplicated(clone); + AddTrack(clone, false); + clones[i] = clone; } + OnTracksOrderChanged(); + if (withUndo && Undo != null && Undo.Enabled) + { + if (clones.Length == 1) + { + Undo.AddAction(new AddRemoveTrackAction(this, clones[0], true)); + } + else + { + var actions = new List(); + for (int i = 0; i < clones.Length; i++) + actions.Add(new AddRemoveTrackAction(this, clones[i], true)); + Undo.AddAction(new MultiUndoAction(actions, "Remove tracks")); + } + } + OnTracksChanged(); + MarkAsEdited(); + SelectedTracks[0].Focus(); } /// @@ -2067,6 +2083,9 @@ namespace FlaxEditor.GUI.Timeline case KeyboardKeys.S: Split(CurrentFrame); return true; + case KeyboardKeys.Delete: + OnKeyframesDelete(null); + return true; } return false; @@ -2145,6 +2164,11 @@ namespace FlaxEditor.GUI.Timeline if (_tracks[i] is IKeyframesEditorContext trackContext) trackContext.OnKeyframesDeselect(editor); } + if (SelectedMedia.Count != 0) + { + SelectedMedia.Clear(); + OnSelectionChanged(); + } } /// @@ -2152,17 +2176,38 @@ namespace FlaxEditor.GUI.Timeline { var globalControl = _backgroundArea; var globalRect = Rectangle.FromPoints(control.PointToParent(globalControl, selection.UpperLeft), control.PointToParent(globalControl, selection.BottomRight)); + var mediaControl = MediaPanel; + var mediaRect = Rectangle.FromPoints(mediaControl.PointFromParent(globalRect.UpperLeft), mediaControl.PointFromParent(globalRect.BottomRight)); + var selectionChanged = false; + if (SelectedMedia.Count != 0) + { + SelectedMedia.Clear(); + selectionChanged = true; + } for (int i = 0; i < _tracks.Count; i++) { if (_tracks[i] is IKeyframesEditorContext trackContext) trackContext.OnKeyframesSelection(editor, globalControl, globalRect); + + foreach (var media in _tracks[i].Media) + { + if (media.Bounds.Intersects(ref mediaRect)) + { + SelectedMedia.Add(media); + selectionChanged = true; + } + } + } + if (selectionChanged) + { + OnSelectionChanged(); } } /// public int OnKeyframesSelectionCount() { - int result = 0; + int result = SelectedMedia.Count; for (int i = 0; i < _tracks.Count; i++) { if (_tracks[i] is IKeyframesEditorContext trackContext) @@ -2179,6 +2224,45 @@ namespace FlaxEditor.GUI.Timeline if (_tracks[i] is IKeyframesEditorContext trackContext) trackContext.OnKeyframesDelete(editor); } + + // Delete selected media events + if (SelectedMedia.Count != 0) + { + if (Undo != null && Undo.Enabled) + { + // Undo per-track + if (_mediaMoveStartTracks == null) + _mediaMoveStartTracks = new List(); + else + _mediaMoveStartTracks.Clear(); + for (var i = 0; i < SelectedMedia.Count; i++) + { + var media = SelectedMedia[i]; + if (!_mediaMoveStartTracks.Contains(media.Track)) + _mediaMoveStartTracks.Add(media.Track); + } + _mediaMoveStartData = new byte[_mediaMoveStartTracks.Count][]; + for (int i = 0; i < _mediaMoveStartData.Length; i++) + _mediaMoveStartData[i] = EditTrackAction.CaptureData(_mediaMoveStartTracks[i]); + } + + foreach (var media in SelectedMedia.ToArray()) + OnDeleteMedia(media); + + if (Undo != null && Undo.Enabled) + { + for (int i = 0; i < _mediaMoveStartData.Length; i++) + { + var track = _mediaMoveStartTracks[i]; + var before = _mediaMoveStartData[i]; + var after = EditTrackAction.CaptureData(track); + if (!Utils.ArraysEqual(before, after)) + AddBatchedUndoAction(new EditTrackAction(this, track, before, after)); + } + } + + MarkAsEdited(); + } } /// @@ -2190,6 +2274,60 @@ namespace FlaxEditor.GUI.Timeline if (_tracks[i] is IKeyframesEditorContext trackContext) trackContext.OnKeyframesMove(editor, _backgroundArea, location, start, end); } + if (SelectedMedia.Count != 0) + { + location = MediaPanel.PointFromParent(location); + if (start) + { + // Start moving selected media events + _mediaMoveStartPos = location; + _mediaMoveStartFrames = new int[SelectedMedia.Count]; + if (_mediaMoveStartTracks == null) + _mediaMoveStartTracks = new List(); + else + _mediaMoveStartTracks.Clear(); + for (var i = 0; i < SelectedMedia.Count; i++) + { + var media = SelectedMedia[i]; + _mediaMoveStartFrames[i] = media.StartFrame; + if (!_mediaMoveStartTracks.Contains(media.Track)) + _mediaMoveStartTracks.Add(media.Track); + } + if (Undo != null && Undo.Enabled) + { + // Undo per-track + _mediaMoveStartData = new byte[_mediaMoveStartTracks.Count][]; + for (int i = 0; i < _mediaMoveStartData.Length; i++) + _mediaMoveStartData[i] = EditTrackAction.CaptureData(_mediaMoveStartTracks[i]); + } + } + else if (end) + { + // End moving selected media events + if (_mediaMoveStartData != null) + { + for (int i = 0; i < _mediaMoveStartData.Length; i++) + { + var track = _mediaMoveStartTracks[i]; + var before = _mediaMoveStartData[i]; + var after = EditTrackAction.CaptureData(track); + if (!Utils.ArraysEqual(before, after)) + AddBatchedUndoAction(new EditTrackAction(this, track, before, after)); + } + } + MarkAsEdited(); + _mediaMoveStartTracks.Clear(); + _mediaMoveStartFrames = null; + } + else + { + // Move selected media events + var moveLocationDelta = location - _mediaMoveStartPos; + var moveDelta = (int)(moveLocationDelta.X / (UnitsPerSecond * Zoom) * FramesPerSecond); + for (var i = 0; i < SelectedMedia.Count; i++) + SelectedMedia[i].StartFrame = _mediaMoveStartFrames[i] + moveDelta; + } + } } /// diff --git a/Source/Editor/GUI/Timeline/Track.cs b/Source/Editor/GUI/Timeline/Track.cs index 6541444b2..935ab863a 100644 --- a/Source/Editor/GUI/Timeline/Track.cs +++ b/Source/Editor/GUI/Timeline/Track.cs @@ -1051,7 +1051,7 @@ namespace FlaxEditor.GUI.Timeline if (CanRename) menu.AddButton("Rename", "F2", StartRenaming); if (CanCopyPaste) - menu.AddButton("Duplicate", "Ctrl+D", () => Timeline.DuplicateSelection()); + menu.AddButton("Duplicate", "Ctrl+D", () => Timeline.DuplicateSelectedTracks()); menu.AddButton("Delete", "Del", Delete); if (CanExpand) { @@ -1292,12 +1292,12 @@ namespace FlaxEditor.GUI.Timeline StartRenaming(); return true; case KeyboardKeys.Delete: - _timeline.DeleteSelection(); + _timeline.DeleteSelectedTracks(); return true; case KeyboardKeys.D: if (Root.GetKey(KeyboardKeys.Control) && CanCopyPaste) { - _timeline.DuplicateSelection(); + _timeline.DuplicateSelectedTracks(); return true; } break;