Add timeline media selection and editing to be global in sync with keyframes editors

This commit is contained in:
Wojtek Figat
2022-01-06 14:39:37 +01:00
parent ff9b116522
commit f4bbf0e348
3 changed files with 289 additions and 116 deletions

View File

@@ -215,7 +215,7 @@ namespace FlaxEditor.GUI.Timeline
/// <summary>
/// Gets a value indicating whether this media can be resized (duration changed).
/// </summary>
public bool CanResize;
public bool CanResize = true;
/// <summary>
/// Initializes a new instance of the <see cref="Media"/> 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();
}

View File

@@ -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<Track> _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
}
/// <summary>
/// Deletes the selected tracks/media events.
/// Deletes the selected tracks.
/// </summary>
/// <param name="withUndo">True if use undo/redo action for track removing.</param>
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<Track>(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<Track>(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<IUndoAction>();
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<IUndoAction>();
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();
}
/// <summary>
@@ -1539,6 +1536,32 @@ namespace FlaxEditor.GUI.Timeline
MarkAsEdited();
}
/// <summary>
/// Adds the media.
/// </summary>
/// <param name="track">The track to add media to.</param>
/// <param name="media">The media to add.</param>
/// <param name="withUndo">True if use undo/redo action for media adding.</param>
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();
}
/// <summary>
/// Called to delete media.
/// </summary>
@@ -1551,89 +1574,82 @@ namespace FlaxEditor.GUI.Timeline
}
/// <summary>
/// Duplicates the selected tracks/media events.
/// Duplicates the selected tracks.
/// </summary>
/// <param name="withUndo">True if use undo/redo action for track duplication.</param>
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<Track>(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<Track>(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<IUndoAction>();
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<IUndoAction>();
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();
}
/// <summary>
@@ -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();
}
}
/// <inheritdoc />
@@ -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();
}
}
/// <inheritdoc />
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<Track>();
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();
}
}
/// <inheritdoc />
@@ -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<Track>();
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;
}
}
}
/// <inheritdoc />

View File

@@ -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;