diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 931d2ce7a..3b3efa062 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -1732,7 +1732,7 @@ namespace FlaxEditor.GUI.Timeline GetTracks(SelectedTracks[i], tracks); } SelectedTracks.Clear(); - if (withUndo & Undo != null) + if (withUndo && Undo != null && Undo.Enabled) { if (tracks.Count == 1) { @@ -1769,7 +1769,7 @@ namespace FlaxEditor.GUI.Timeline // Delete tracks var tracks = new List(4); GetTracks(track, tracks); - if (withUndo & Undo != null) + if (withUndo && Undo != null && Undo.Enabled) { if (tracks.Count == 1) { @@ -1803,6 +1803,93 @@ namespace FlaxEditor.GUI.Timeline track.OnDeleted(); } + /// + /// Duplicates the selected tracks/media events. + /// + /// True if use undo/redo action for track duplication. + public void DuplicateSelection(bool withUndo = true) + { + if (SelectedMedia.Count > 0) + { + throw new NotImplementedException("TODO: duplicating selected media events"); + } + + if (SelectedTracks.Count > 0) + { + // Duplicate selected tracks + var tracks = new List(SelectedTracks.Count); + for (int i = 0; i < SelectedTracks.Count; i++) + { + GetTracks(SelectedTracks[i], tracks); + } + var clones = new Track[tracks.Count]; + for (int i = 0; i < tracks.Count; i++) + { + var track = tracks[i]; + var options = new TrackCreateOptions + { + Archetype = track.Archetype, + Loop = track.Loop, + Mute = track.Mute, + }; + 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) + { + trackParent = clones[j]; + break; + } + } + trackIndex--; + } + 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(); + } + } + /// /// Called once to setup the drag drop handling for the timeline (lazy init on first drag action). /// @@ -1945,6 +2032,20 @@ namespace FlaxEditor.GUI.Timeline return !string.IsNullOrEmpty(name) && _tracks.All(x => x.Name != name); } + /// + /// Gets the name of the track that is valid to use for a timeline. + /// + /// The name. + /// The track name. + public string GetValidTrackName(string name) + { + string newName = name; + int count = 0; + while (!IsTrackNameValid(newName)) + newName = string.Format("{0} {1}", name, count++); + return newName; + } + /// /// Arranges the tracks. /// @@ -2185,7 +2286,7 @@ namespace FlaxEditor.GUI.Timeline editor.Select(obj); _timeline = timeline; - if (timeline.Undo != null && undoContext != null) + if (timeline.Undo != null && timeline.Undo.Enabled && undoContext != null) { _undoContext = undoContext; if (undoContext is Track track) diff --git a/Source/Editor/GUI/Timeline/Track.cs b/Source/Editor/GUI/Timeline/Track.cs index fc730484d..78cd07d55 100644 --- a/Source/Editor/GUI/Timeline/Track.cs +++ b/Source/Editor/GUI/Timeline/Track.cs @@ -380,6 +380,14 @@ namespace FlaxEditor.GUI.Timeline Dispose(); } + /// + /// Called when track gets duplicated. + /// + /// The cloned track instance. + public virtual void OnDuplicated(Track clone) + { + } + /// /// Arranges the track and all its media. Called when timeline performs layout for the contents. /// @@ -715,6 +723,11 @@ namespace FlaxEditor.GUI.Timeline /// public virtual bool CanRename => true; + /// + /// Gets a value indicating whether user can copy and paste this track. + /// + public virtual bool CanCopyPaste => true; + /// /// Gets a value indicating whether user can expand the track contents of the inner hierarchy. /// @@ -1248,6 +1261,13 @@ namespace FlaxEditor.GUI.Timeline case KeyboardKeys.Delete: _timeline.DeleteSelection(); return true; + case KeyboardKeys.D: + if (Root.GetKey(KeyboardKeys.Control) && CanCopyPaste) + { + _timeline.DuplicateSelection(); + return true; + } + break; case KeyboardKeys.ArrowLeft: if (CanExpand && IsExpanded) { diff --git a/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs b/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs index 6ad3e95bc..8e5159ed2 100644 --- a/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs @@ -533,6 +533,9 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// public override bool CanCopyPaste => false; + /// + public override bool CanExpand => true; + /// public override void OnParentTrackChanged(Track parent) { @@ -631,6 +634,14 @@ namespace FlaxEditor.GUI.Timeline.Tracks UpdatePreviewValue(); } + /// + public override void OnDuplicated(Track clone) + { + base.OnDuplicated(clone); + + clone.Name = Guid.NewGuid().ToString("N"); + } + /// public override void OnDestroy() { diff --git a/Source/Editor/GUI/Timeline/Tracks/MemberTrack.cs b/Source/Editor/GUI/Timeline/Tracks/MemberTrack.cs index 5b9ffadd2..c906ae91f 100644 --- a/Source/Editor/GUI/Timeline/Tracks/MemberTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/MemberTrack.cs @@ -288,6 +288,9 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// public override bool CanRename => false; + /// + public override bool CanCopyPaste => false; + /// /// Called when member gets changed. /// @@ -314,6 +317,14 @@ namespace FlaxEditor.GUI.Timeline.Tracks } } + /// + public override void OnDuplicated(Track clone) + { + base.OnDuplicated(clone); + + clone.Name = Guid.NewGuid().ToString("N"); + } + private void OnTimelineShowPreviewValuesChanged() { _previewValue.Visible = Timeline.ShowPreviewValues; diff --git a/Source/Editor/GUI/Timeline/Undo/AddRemoveTrackAction.cs b/Source/Editor/GUI/Timeline/Undo/AddRemoveTrackAction.cs index be5beb757..05414d076 100644 --- a/Source/Editor/GUI/Timeline/Undo/AddRemoveTrackAction.cs +++ b/Source/Editor/GUI/Timeline/Undo/AddRemoveTrackAction.cs @@ -44,7 +44,10 @@ namespace FlaxEditor.GUI.Timeline.Undo { var track = _timeline.FindTrack(_name); if (track != null) + { + Editor.LogWarning($"Cannot add track {_name}. It already exists."); return; + } track = _options.Archetype.Create(_options); track.Name = _name; track.Color = _color; @@ -64,6 +67,11 @@ namespace FlaxEditor.GUI.Timeline.Undo private void Remove() { var track = _timeline.FindTrack(_name); + if (track == null) + { + Editor.LogWarning($"Cannot remove track {_name}. It doesn't already exists."); + return; + } _timeline.Delete(track, false); }