diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 992a4683e..28b6ab73d 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -244,10 +244,7 @@ const QList &TrackOutput::Blocks() const void TrackOutput::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *source) { if (block_invalidate_cache_stack_ == 0) { - if (queued_length_change_) { - queued_length_change_ = false; - SetLengthInternal(queued_length_); - } + PushLengthChangeSignal(); Node::InvalidateCache(TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), track_length())), from, source); } @@ -437,6 +434,14 @@ void TrackOutput::Hash(QCryptographicHash &hash, const rational &time) const } } +void TrackOutput::PushLengthChangeSignal() +{ + if (queued_length_change_) { + queued_length_change_ = false; + SetLengthInternal(queued_length_); + } +} + void TrackOutput::SetTrackName(const QString &name) { track_name_ = name; diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 6b321cb16..a17f82ea9 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -193,6 +193,8 @@ public: virtual void Hash(QCryptographicHash& hash, const rational &time) const override; + void PushLengthChangeSignal(); + public slots: void SetTrackName(const QString& name); diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 48fd1ba61..95c982e4a 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -739,33 +739,43 @@ void TimelineWidget::DeleteInToOut(bool ripple) QUndoCommand* command = new QUndoCommand(); - foreach (TrackOutput* track, GetConnectedNode()->GetTracks()) { - if (!track->IsLocked()) { - if (ripple) { - new TrackRippleRemoveAreaCommand(track, - GetConnectedTimelinePoints()->workarea()->in(), - GetConnectedTimelinePoints()->workarea()->out(), - command); - } else { - GapBlock* gap = new GapBlock(); + if (ripple) { - gap->set_length_and_media_out(GetConnectedTimelinePoints()->workarea()->length()); + new TimelineRippleRemoveAreaCommand(GetConnectedNode(), + GetConnectedTimelinePoints()->workarea()->in(), + GetConnectedTimelinePoints()->workarea()->out(), + command); - new NodeAddCommand(static_cast(track->parent()), - gap, - command); + } else { + QVector unlocked_tracks = GetConnectedNode()->GetUnlockedTracks(); - new TrackPlaceBlockCommand(GetConnectedNode()->track_list(track->track_type()), - track->Index(), - gap, - GetConnectedTimelinePoints()->workarea()->in(), - command); - } + foreach (TrackOutput* track, unlocked_tracks) { + GapBlock* gap = new GapBlock(); + + gap->set_length_and_media_out(GetConnectedTimelinePoints()->workarea()->length()); + + new NodeAddCommand(static_cast(track->parent()), + gap, + command); + + new TrackPlaceBlockCommand(GetConnectedNode()->track_list(track->track_type()), + track->Index(), + gap, + GetConnectedTimelinePoints()->workarea()->in(), + command); } } // Clear workarea after this - new WorkareaSetEnabledCommand(GetTimelinePointsProject(), GetConnectedTimelinePoints(), false, command); + new WorkareaSetEnabledCommand(GetTimelinePointsProject(), + GetConnectedTimelinePoints(), + false, + command); + + if (ripple) { + SetTimeAndSignal(Timecode::time_to_timestamp(GetConnectedTimelinePoints()->workarea()->in(), + timebase())); + } Core::instance()->undo_stack()->push(command); } @@ -1298,6 +1308,10 @@ void TimelineWidget::RippleTo(Timeline::MovementMode mode) QVector tracks = GetEditToInfo(playhead_time, mode); + if (tracks.isEmpty()) { + return; + } + // 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; @@ -1325,29 +1339,17 @@ void TimelineWidget::RippleTo(Timeline::MovementMode mode) rational in_ripple = qMin(closest_point_to_playhead, playhead_time); rational out_ripple = qMax(closest_point_to_playhead, playhead_time); - QUndoCommand* command = new QUndoCommand(); + TimelineRippleRemoveAreaCommand* c = new TimelineRippleRemoveAreaCommand(GetConnectedNode(), + in_ripple, + out_ripple); - foreach (const Timeline::EditToInfo& info, tracks) { - TrackOutput* track = info.track; + Core::instance()->undo_stack()->push(c); - // 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; + // 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())); } } diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index 7d9c5221a..ed441b5de 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -103,47 +103,27 @@ void TimelineWidget::RippleTool::FinishDrag(TimelineViewMouseEvent *event) { Q_UNUSED(event) - // For ripple operations, all ghosts will be moving the same way - Timeline::MovementMode movement_mode = parent()->ghost_items_.first()->mode(); + QVector< QList > info_list(Timeline::kTrackTypeCount); + + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + TrackOutput* track = parent()->GetTrackFromReference(ghost->Track()); + + TrackListRippleToolCommand::RippleInfo i = {Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)), + Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kReferenceBlock)), + track, + ghost->AdjustedLength(), + ghost->Length()}; + + info_list[track->track_type()].append(i); + } QUndoCommand* command = new QUndoCommand(); - // Find earliest point to ripple around - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (!b) { - // This is a gap we are creating - - // Make sure there's actually a gap being created - if (ghost->AdjustedLength() > 0) { - GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(ghost->AdjustedLength()); - new NodeAddCommand(static_cast(parent()->GetConnectedNode()->parent()), gap, command); - - Block* block_to_append_gap_to = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kReferenceBlock)); - - new TrackInsertBlockAfterCommand(parent()->GetTrackFromReference(ghost->Track()), - gap, - block_to_append_gap_to, - command); - } - } else { - // This was a Block that already existed - if (ghost->AdjustedLength() > 0) { - if (movement_mode == Timeline::kTrimIn) { - // We'll need to shift the media in point too - new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command); - } else { - new BlockResizeCommand(b, ghost->AdjustedLength(), command); - } - } else { - // Assume the Block was a Gap and it was reduced to zero length, remove it here - new TrackRippleRemoveBlockCommand(parent()->GetTrackFromReference(ghost->Track()), b, command); - - new NodeRemoveWithExclusiveDeps(static_cast(b->parent()), b, command); - } - } + for (int i=0;iGetConnectedNode()->track_list(static_cast(i)), + info_list.at(i), + drag_movement_mode(), + command); } Core::instance()->undo_stack()->pushIfHasChildren(command); diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index a9d624c6b..282b4d587 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -1311,4 +1311,257 @@ void TrackSlideCommand::slide_internal(bool undo) } } +TrackListRippleRemoveAreaCommand::TrackListRippleRemoveAreaCommand(TrackList *list, rational in, rational out, QUndoCommand *parent) : + UndoCommand(parent), + list_(list), + in_(in), + out_(out) +{ + all_tracks_unlocked_ = true; + + foreach (TrackOutput* track, list_->GetTracks()) { + if (track->IsLocked()) { + all_tracks_unlocked_ = false; + continue; + } + + TrackRippleRemoveAreaCommand* c = new TrackRippleRemoveAreaCommand(track, in, out); + commands_.append(c); + working_tracks_.append(track); + } +} + +TrackListRippleRemoveAreaCommand::~TrackListRippleRemoveAreaCommand() +{ + qDeleteAll(commands_); +} + +Project *TrackListRippleRemoveAreaCommand::GetRelevantProject() const +{ + return static_cast(static_cast(list_->parent())->parent())->project(); +} + +void TrackListRippleRemoveAreaCommand::redo_internal() +{ + if (all_tracks_unlocked_) { + // We can optimize here by simply shifting the whole cache forward instead of re-caching + // everything following this time + foreach (TrackOutput* track, working_tracks_) { + track->BlockInvalidateCache(); + } + } + + foreach (TrackRippleRemoveAreaCommand* c, commands_) { + c->redo(); + } + + if (all_tracks_unlocked_) { + // Shift cache + if (list_->type() == Timeline::kTrackTypeVideo) { + static_cast(list_->parent())->video_frame_cache()->Shift(out_, in_); + } else if (list_->type() == Timeline::kTrackTypeAudio) { + static_cast(list_->parent())->audio_playback_cache()->Shift(out_, in_); + } + + foreach (TrackOutput* track, working_tracks_) { + track->UnblockInvalidateCache(); + track->PushLengthChangeSignal(); + } + } +} + +void TrackListRippleRemoveAreaCommand::undo_internal() +{ + if (all_tracks_unlocked_) { + // We can optimize here by simply shifting the whole cache forward instead of re-caching + // everything following this time + foreach (TrackOutput* track, working_tracks_) { + track->BlockInvalidateCache(); + } + } + + foreach (TrackRippleRemoveAreaCommand* c, commands_) { + c->undo(); + } + + if (all_tracks_unlocked_) { + // Shift cache back + foreach (TrackOutput* track, working_tracks_) { + track->UnblockInvalidateCache(); + track->PushLengthChangeSignal(); + } + + if (list_->type() == Timeline::kTrackTypeVideo) { + static_cast(list_->parent())->video_frame_cache()->Shift(in_, out_); + } else if (list_->type() == Timeline::kTrackTypeAudio) { + static_cast(list_->parent())->audio_playback_cache()->Shift(in_, out_); + } + } +} + +TimelineRippleRemoveAreaCommand::TimelineRippleRemoveAreaCommand(ViewerOutput *timeline, rational in, rational out, QUndoCommand *parent) : + UndoCommand(parent), + timeline_(timeline) +{ + for (int i=0; itrack_list(static_cast(i)), + in, + out, + this); + } +} + +Project *TimelineRippleRemoveAreaCommand::GetRelevantProject() const +{ + return static_cast(timeline_->parent())->project(); +} + +TrackListRippleToolCommand::TrackListRippleToolCommand(TrackList *track_list, const QList &info, const Timeline::MovementMode &movement_mode, QUndoCommand *parent) : + UndoCommand(parent), + track_list_(track_list), + info_(info), + movement_mode_(movement_mode) +{ + working_data_.resize(info_.size()); + + all_tracks_unlocked_ = (info_.size() == track_list_->GetTrackCount()); +} + +Project *TrackListRippleToolCommand::GetRelevantProject() const +{ + return static_cast(static_cast(track_list_->parent())->parent())->project(); +} + +void TrackListRippleToolCommand::redo_internal() +{ + rational old_latest_pt; + rational earliest_pt; + + if (all_tracks_unlocked_) { + // We can do some optimization here + foreach (const RippleInfo& info, info_) { + info.track->BlockInvalidateCache(); + } + + old_latest_pt = RATIONAL_MIN; + earliest_pt = RATIONAL_MAX; + foreach (const RippleInfo& info, info_) { + if (info.block) { + old_latest_pt = qMax(old_latest_pt, info.block->out()); + + if (movement_mode_ == Timeline::kTrimIn) { + earliest_pt = qMin(earliest_pt, info.block->in()); + } else { + earliest_pt = qMin(earliest_pt, info.block->out()); + } + } else { + old_latest_pt = qMax(old_latest_pt, info.ref_block->out()); + earliest_pt = qMin(earliest_pt, info.ref_block->out()); + } + } + } + + for (int i=0;i 0) { + if (movement_mode_ == Timeline::kTrimIn) { + // We'll need to shift the media in point too + b->set_length_and_media_in(info.new_length); + } else { + b->set_length_and_media_out(info.new_length); + } + } else { + // Assume the Block was a Gap and it was reduced to zero length, remove it here + working_data_[i].removed_gap_after = b->previous(); + info.track->RippleRemoveBlock(b); + TakeNodeFromParentGraph(b, &memory_manager_); + } + } else if (info.new_length > 0) { + // This is a gap we are creating + GapBlock* gap = new GapBlock(); + gap->set_length_and_media_out(info.new_length); + static_cast(info.ref_block->parent())->AddNode(gap); + working_data_[i].created_gap = gap; + + info.track->InsertBlockAfter(gap, info.ref_block); + } + } + + if (all_tracks_unlocked_) { + // We can do some optimization here + + rational new_latest_pt = RATIONAL_MIN; + for (int i=0;iout()); + } else { + new_latest_pt = qMax(new_latest_pt, working_data_.at(i).created_gap->out()); + } + } + + if (track_list_->type() == Timeline::kTrackTypeVideo) { + static_cast(track_list_->parent())->video_frame_cache()->Shift(old_latest_pt, new_latest_pt); + } else if (track_list_->type() == Timeline::kTrackTypeAudio) { + static_cast(track_list_->parent())->audio_playback_cache()->Shift(old_latest_pt, new_latest_pt); + } + + foreach (const RippleInfo& info, info_) { + info.track->UnblockInvalidateCache(); + + // FIXME: Untested, is this desirable behavior? + if (earliest_pt < new_latest_pt) { + info.track->InvalidateCache(TimeRange(earliest_pt, new_latest_pt), + info.track->block_input(), + info.track->block_input()); + } + } + } +} + +void TrackListRippleToolCommand::undo_internal() +{ + // Clean created gaps + for (int i=info_.size()-1; i>=0; i--) { + const RippleInfo& info = info_.at(i); + + Block* b = info.block; + + if (b) { + // This was a Block that already existed + if (info.new_length > 0) { + if (movement_mode_ == Timeline::kTrimIn) { + // We'll need to shift the media in point too + b->set_length_and_media_in(info.old_length); + } else { + b->set_length_and_media_out(info.old_length); + } + } else { + // Assume the Block was a Gap and it was reduced to zero length, remove it here + Block* previous_block = working_data_[i].removed_gap_after; + + static_cast(info.track->parent())->AddNode(b); + + if (previous_block) { + info.track->InsertBlockAfter(b, previous_block); + } else { + info.track->PrependBlock(b); + } + } + } else if (info.new_length > 0) { + // We created a gap here, remove it + GapBlock* gap = working_data_.at(i).created_gap; + + info.track->RippleRemoveBlock(gap); + delete TakeNodeFromParentGraph(gap); + } + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/undo/undo.h b/app/widget/timelinewidget/undo/undo.h index 8d6957ca5..892c21daa 100644 --- a/app/widget/timelinewidget/undo/undo.h +++ b/app/widget/timelinewidget/undo/undo.h @@ -223,6 +223,84 @@ protected: }; +class TrackListRippleRemoveAreaCommand : public UndoCommand { +public: + TrackListRippleRemoveAreaCommand(TrackList* list, rational in, rational out, QUndoCommand* parent = nullptr); + + virtual ~TrackListRippleRemoveAreaCommand() override; + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + TrackList* list_; + + QList working_tracks_; + + rational in_; + + rational out_; + + bool all_tracks_unlocked_; + + QVector commands_; + +}; + +class TimelineRippleRemoveAreaCommand : public UndoCommand { +public: + TimelineRippleRemoveAreaCommand(ViewerOutput* timeline, rational in, rational out, QUndoCommand* parent = nullptr); + + virtual Project* GetRelevantProject() const override; + +private: + ViewerOutput* timeline_; + +}; + +class TrackListRippleToolCommand : public UndoCommand { +public: + struct RippleInfo { + Block* block; + Block* ref_block; + TrackOutput* track; + rational new_length; + rational old_length; + }; + + TrackListRippleToolCommand(TrackList* track_list, + const QList& info, + const Timeline::MovementMode& movement_mode, + QUndoCommand* parent = nullptr); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + TrackList* track_list_; + + QList info_; + Timeline::MovementMode movement_mode_; + + struct WorkingData { + GapBlock* created_gap; + Block* removed_gap_after; + }; + + QVector working_data_; + + QObject memory_manager_; + + bool all_tracks_unlocked_; + +}; + /** * @brief Destructively places `block` at the in point `start` *