From acee6c595892311002101f787fb3108b0025a294 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 31 May 2020 01:21:08 +1000 Subject: [PATCH] timeline: reworked ripple/edit to behavior and documented some track behavior --- app/node/output/track/track.cpp | 1 + app/node/output/track/track.h | 45 +++- app/node/output/viewer/viewer.cpp | 14 + app/node/output/viewer/viewer.h | 5 + app/widget/timelinewidget/timelinewidget.cpp | 257 +++++++++++++------ app/widget/timelinewidget/timelinewidget.h | 14 +- 6 files changed, 253 insertions(+), 83 deletions(-) diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 6dfb09a56..ef971fb6f 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -225,6 +225,7 @@ QList TrackOutput::BlocksAtTimeRange(const TimeRange &range) const foreach (Block* block, block_cache_) { if (block + && block->is_enabled() && block->out() > range.in() && block->in() < range.out()) { list.append(block); diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 21df57954..1cb04f451 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -57,29 +57,68 @@ public: const int& Index(); void SetIndex(const int& index); + /** + * @brief Returns the block that starts BEFORE (not AT) and ends AFTER (not AT) a time + * + * Catches the first block that matches `block.in < time && block.out > time` or nullptr if any + * block starts/ends precisely at that time or the time exceeds the track length. + */ Block* BlockContainingTime(const rational& time) const; /** - * @brief Returns the block that starts BEFORE a given time and ends some time AFTER or precisely AT that time + * @brief Returns the block that starts BEFORE a given time and ends either AFTER or AT that time + * + * @return Catches the first block that matches `block.out >= time` or nullptr if this time + * exceeds the track length. */ Block* NearestBlockBefore(const rational& time) const; /** - * @brief Returns the block that starts BEFORE a given time OR the block that starts precisely at that time + * @brief Returns the block that starts BEFORE or AT a given time. + * + * @return Catches the first block that matches `block.out > time` or nullptr if this time + * exceeds the track length. */ Block* NearestBlockBeforeOrAt(const rational& time) const; /** - * @brief Returns the block that starts either precisely AT a given time or the soonest block AFTER + * @brief Returns the block that starts either AT a given time or the soonest block AFTER + * + * @return Catches the first block that matches `block.in >= time` or nullptr if this time + * exceeds the track length. */ Block* NearestBlockAfterOrAt(const rational& time) const; /** * @brief Returns the block that starts AFTER the given time (but never AT the given time) + * + * @return Catches the first block that matches `block.in > time` or nullptr if this time + * exceeds the track length. */ Block* NearestBlockAfter(const rational& time) const; + /** + * @brief Returns the block that should be rendered/visible at a given time + * + * Use this for any video rendering or determining which block will actually be active at any + * time. + * + * @return Catches the first block that matches `block.in <= time && block.out > time`. Returns + * nullptr if the time exceeds the track length, the block active at this time is disabled, or + * if IsMuted() is true. + */ Block* BlockAtTime(const rational& time) const; + + /** + * @brief Returns a list of blocks that should be rendered/visible during a given time range + * + * Use this for audio rendering to determine all blocks that will be active throughout a range + * of time. + * + * @return Similar to BlockAtTime() but will match several blocks where + * `block.in < range.out && block.out > range.in`. Returns an empty list if IsMuted() or if + * `range.in >= track.length`. Blocks that are not enabled will be omitted from the returned list. + */ QList BlocksAtTimeRange(const TimeRange& range) const; const QList &Blocks() const; diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index f918d8061..f8da01579 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -130,6 +130,20 @@ rational ViewerOutput::GetLength() return last_length_; } +QVector ViewerOutput::GetUnlockedTracks() const +{ + QVector tracks = GetTracks(); + + for (int i=0;iIsLocked()) { + tracks.removeAt(i); + i--; + } + } + + return tracks; +} + void ViewerOutput::UpdateTrackCache() { track_cache_.clear(); diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 694f8d274..2b188e5da 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -85,6 +85,11 @@ public: return track_cache_; } + /** + * @brief Same as GetTracks() but omits tracks that are locked. + */ + QVector GetUnlockedTracks() const; + NodeInput* track_input(Timeline::TrackType type) const { return track_inputs_.at(type); } diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 914043378..26697155f 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -394,22 +394,22 @@ void TimelineWidget::DeselectAll() void TimelineWidget::RippleToIn() { - RippleEditTo(Timeline::kTrimIn, false); + RippleTo(Timeline::kTrimIn); } void TimelineWidget::RippleToOut() { - RippleEditTo(Timeline::kTrimOut, false); + RippleTo(Timeline::kTrimOut); } void TimelineWidget::EditToIn() { - RippleEditTo(Timeline::kTrimIn, true); + EditTo(Timeline::kTrimIn); } void TimelineWidget::EditToOut() { - RippleEditTo(Timeline::kTrimOut, true); + EditTo(Timeline::kTrimOut); } void TimelineWidget::SplitAtPlayhead() @@ -787,80 +787,6 @@ QList TimelineWidget::GetSelectedBlocks() return list; } -void TimelineWidget::RippleEditTo(Timeline::MovementMode mode, bool insert_gaps) -{ - rational playhead_time = GetTime(); - - rational closest_point_to_playhead; - if (mode == Timeline::kTrimIn) { - closest_point_to_playhead = 0; - } else { - closest_point_to_playhead = RATIONAL_MAX; - } - - foreach (TrackOutput* track, GetConnectedNode()->GetTracks()) { - Block* b; - - if (mode == Timeline::kTrimIn) { - b = track->NearestBlockBeforeOrAt(playhead_time); - } else { - b = track->NearestBlockBefore(playhead_time); - } - - if (b) { - if (mode == Timeline::kTrimIn) { - closest_point_to_playhead = qMax(b->in(), closest_point_to_playhead); - } else { - closest_point_to_playhead = qMin(b->out(), closest_point_to_playhead); - } - } - } - - QUndoCommand* command = new QUndoCommand(); - - bool single_frame_mode = (!insert_gaps && closest_point_to_playhead == playhead_time); - - if (single_frame_mode) { - // Remove one frame only - if (mode == Timeline::kTrimIn) { - playhead_time += timebase(); - } else { - playhead_time -= timebase(); - } - } - - rational in_ripple = qMin(closest_point_to_playhead, playhead_time); - rational out_ripple = qMax(closest_point_to_playhead, playhead_time); - rational ripple_length = out_ripple - in_ripple; - - foreach (TrackOutput* track, GetConnectedNode()->GetTracks()) { - GapBlock* gap = nullptr; - if (insert_gaps) { - gap = new GapBlock(); - gap->set_length_and_media_out(ripple_length); - new NodeAddCommand(static_cast(track->parent()), gap, command); - } - - TrackRippleRemoveAreaCommand* ripple_command = new TrackRippleRemoveAreaCommand(track, - in_ripple, - out_ripple, - command); - - if (insert_gaps) { - ripple_command->SetInsert(gap); - } - } - - Core::instance()->undo_stack()->pushIfHasChildren(command); - - // Jump to where new cut is if applicable - if (mode == Timeline::kTrimIn && !insert_gaps) { - SetTimeAndSignal(Timecode::time_to_timestamp(closest_point_to_playhead, timebase())); - } else if (mode == Timeline::kTrimOut && single_frame_mode) { - SetTimeAndSignal(Timecode::time_to_timestamp(playhead_time, timebase())); - } -} - void TimelineWidget::InsertGapsAt(const rational &earliest_point, const rational &insert_length, QUndoCommand *command) { QVector blocks_to_split; @@ -1299,6 +1225,181 @@ void TimelineWidget::SetBlockLinksSelected(Block* block, bool selected) } } +QVector TimelineWidget::GetEditToInfo(const rational& playhead_time, + Timeline::MovementMode mode) +{ + // Get list of unlocked tracks + QVector tracks = GetConnectedNode()->GetUnlockedTracks(); + + // Create list to cache nearest times and the blocks at this point + QVector info_list(tracks.size()); + + for (int i=0;iNearestBlockBeforeOrAt(playhead_time); + } else { + b = track->NearestBlockBefore(playhead_time); + } + + // If we have a block here, cache how close it is to the track + if (b) { + rational this_track_closest_point; + + if (mode == Timeline::kTrimIn) { + this_track_closest_point = b->in(); + } else { + this_track_closest_point = b->out(); + } + + info.nearest_time = this_track_closest_point; + } + + info.nearest_block = b; + + info_list[i] = info; + } + + return info_list; +} + +void TimelineWidget::RippleTo(Timeline::MovementMode mode) +{ + rational playhead_time = GetTime(); + + QVector tracks = GetEditToInfo(playhead_time, mode); + + // Find each track's nearest point and determine the overall timeline's nearest point + rational closest_point_to_playhead = (mode == Timeline::kTrimIn) ? rational() : RATIONAL_MAX; + + foreach (const EditToInfo& info, tracks) { + if (info.nearest_block) { + if (mode == Timeline::kTrimIn) { + closest_point_to_playhead = qMax(info.nearest_time, closest_point_to_playhead); + } else { + closest_point_to_playhead = qMin(info.nearest_time, closest_point_to_playhead); + } + } + } + + // If we're not inserting gaps and the edit point is right on the nearest in point, we enter a + // single-frame mode where we remove one frame only + if (closest_point_to_playhead == playhead_time) { + if (mode == Timeline::kTrimIn) { + playhead_time += timebase(); + } else { + playhead_time -= timebase(); + } + } + + // For standard rippling, we can cache here the region that will be rippled out + rational in_ripple = qMin(closest_point_to_playhead, playhead_time); + rational out_ripple = qMax(closest_point_to_playhead, playhead_time); + + QUndoCommand* command = new QUndoCommand(); + + foreach (const EditToInfo& info, tracks) { + TrackOutput* track = info.track; + + // Simply remove this region + new TrackRippleRemoveAreaCommand(track, + in_ripple, + out_ripple, + command); + } + + if (command->childCount() > 0) { + Core::instance()->undo_stack()->pushIfHasChildren(command); + + // If we rippled, ump to where new cut is if applicable + if (mode == Timeline::kTrimIn) { + SetTimeAndSignal(Timecode::time_to_timestamp(closest_point_to_playhead, timebase())); + } else if (mode == Timeline::kTrimOut && closest_point_to_playhead == GetTime()) { + SetTimeAndSignal(Timecode::time_to_timestamp(playhead_time, timebase())); + } + } else { + delete command; + } +} + +void TimelineWidget::EditTo(Timeline::MovementMode mode) +{ + const rational playhead_time = GetTime(); + + // Get list of unlocked tracks + QVector tracks = GetEditToInfo(playhead_time, mode); + + QUndoCommand* command = new QUndoCommand(); + + foreach (const EditToInfo& info, tracks) { + TrackOutput* track = info.track; + + Block* block_here = info.nearest_block; + + // Check if this track's nearest time was the playhead or if there's no block here, in which + // case this is a no-op + if (!block_here || info.nearest_time == playhead_time) { + continue; + } + + GapBlock* gap = nullptr; + + // Resize the block at this time + if (mode == Timeline::kTrimIn) { + rational trim_length = playhead_time - block_here->in(); + + gap = new GapBlock(); + gap->set_length_and_media_out(trim_length); + new NodeAddCommand(static_cast(track->parent()), gap, command); + + qDebug() << "Resizing" << block_here->length().toDouble() << "to" << (block_here->length() - trim_length).toDouble(); + + new BlockResizeWithMediaInCommand(block_here, + block_here->length() - trim_length, + command); + + if (block_here->previous()) { + new TrackInsertBlockAfterCommand(track, gap, block_here->previous(), command); + } else { + new TrackPrependBlockCommand(track, gap, command); + } + } else { + rational trim_length = block_here->out() - playhead_time; + + new BlockResizeCommand(block_here, + block_here->length() - trim_length, + command); + + // We only need to add a gap if there's actually a block after this one, otherwise it + // doesn't matter + if (block_here->next()) { + gap = new GapBlock(); + gap->set_length_and_media_out(trim_length); + new NodeAddCommand(static_cast(track->parent()), gap, command); + + new TrackInsertBlockAfterCommand(track, gap, block_here, command); + } + } + + // If a gap was added, clean the gaps on this track too + if (gap) { + new TrackCleanGapsCommand(GetConnectedNode()->track_list(track->track_type()), + track->Index(), + command); + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); +} + void TimelineWidget::StartRubberBandSelect(bool enable_selecting, bool select_links) { drag_origin_ = QCursor::pos(); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 3d96b9bfe..89094d80f 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -425,6 +425,18 @@ private: void SetBlockLinksSelected(Block *block, bool selected); + struct EditToInfo { + TrackOutput* track; + rational nearest_time; + Block* nearest_block; + }; + + QVector GetEditToInfo(const rational &playhead_time, Timeline::MovementMode mode); + + void RippleTo(Timeline::MovementMode mode); + + void EditTo(Timeline::MovementMode mode); + QPoint drag_origin_; void StartRubberBandSelect(bool enable_selecting, bool select_links); @@ -449,8 +461,6 @@ private: QMap block_items_; - void RippleEditTo(Timeline::MovementMode mode, bool insert_gaps); - TrackOutput* GetTrackFromReference(const TrackReference& ref); void ConnectViewSelectionSignal(TimelineView* view);