From e2a590da26b2102d1d9b5d1ce21d2bd5205df1cd Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 26 Apr 2021 20:29:21 +1000 Subject: [PATCH] timeline: remove transitions when removing clips Fixes #1224 --- app/node/block/transition/transition.cpp | 10 - app/widget/timelinewidget/timelineundo.cpp | 172 ++++++++++ app/widget/timelinewidget/timelineundo.h | 341 +++++++------------ app/widget/timelinewidget/timelinewidget.cpp | 4 +- tests/CMakeLists.txt | 6 +- tests/timeline/timeline-tests.cpp | 276 ++++++++++++++- 6 files changed, 564 insertions(+), 245 deletions(-) diff --git a/app/node/block/transition/transition.cpp b/app/node/block/transition/transition.cpp index cc77b0dd8..957a48889 100644 --- a/app/node/block/transition/transition.cpp +++ b/app/node/block/transition/transition.cpp @@ -249,21 +249,11 @@ void TransitionBlock::InputConnectedEvent(const QString &input, int element, con if (input == kOutBlockInput) { // If node is not a block, this will just be null if ((connected_out_block_ = dynamic_cast(output.node()))) { - - Q_ASSERT(!dynamic_cast(connected_out_block_) - && !connected_out_block_->out_transition() - && connected_out_block_ == this->previous()); - connected_out_block_->set_out_transition(this); } } else if (input == kInBlockInput) { // If node is not a block, this will just be null if ((connected_in_block_ = dynamic_cast(output.node()))) { - - Q_ASSERT(!dynamic_cast(connected_in_block_) - && !connected_in_block_->in_transition() - && connected_in_block_ == this->next()); - connected_in_block_->set_in_transition(this); } } diff --git a/app/widget/timelinewidget/timelineundo.cpp b/app/widget/timelinewidget/timelineundo.cpp index 3f4610324..b48cdd452 100644 --- a/app/widget/timelinewidget/timelineundo.cpp +++ b/app/widget/timelinewidget/timelineundo.cpp @@ -200,4 +200,176 @@ void BlockTrimCommand::prep() } } +void TrackReplaceBlockWithGapCommand::redo() +{ + // Determine if this block is connected to any transitions that should also be removed by this operation + if (transition_remove_commands_.isEmpty()) { + CreateRemoveTransitionCommandIfNecessary(false); + CreateRemoveTransitionCommandIfNecessary(true); + } + for (auto it=transition_remove_commands_.cbegin(); it!=transition_remove_commands_.cend(); it++) { + (*it)->redo(); + } + + if (block_->next()) { + track_->BeginOperation(); + + // Invalidate the range inhabited by this block + TimeRange invalidate_range(block_->in(), block_->out()); + + // Block has a next, which means it's NOT at the end of the sequence and thus requires a gap + rational new_gap_length = block_->length(); + + Block* previous = block_->previous(); + Block* next = block_->next(); + + bool previous_is_a_gap = dynamic_cast(previous); + bool next_is_a_gap = dynamic_cast(next); + + if (previous_is_a_gap && next_is_a_gap) { + // Clip is preceded and followed by a gap, so we'll merge the two + existing_gap_ = static_cast(previous); + + existing_merged_gap_ = static_cast(next); + new_gap_length += existing_merged_gap_->length(); + track_->RippleRemoveBlock(existing_merged_gap_); + existing_merged_gap_->setParent(&memory_manager_); + } else if (previous_is_a_gap) { + // Extend this gap to fill space left by block + existing_gap_ = static_cast(previous); + } else if (next_is_a_gap) { + // Extend this gap to fill space left by block + existing_gap_ = static_cast(next); + } + + if (existing_gap_) { + // Extend an existing gap + new_gap_length += existing_gap_->length(); + existing_gap_->set_length_and_media_out(new_gap_length); + track_->RippleRemoveBlock(block_); + + existing_gap_precedes_ = (existing_gap_ == previous); + } else { + // No gap exists to fill this space, create a new one and swap it in + if (!our_gap_) { + our_gap_ = new GapBlock(); + our_gap_->set_length_and_media_out(new_gap_length); + } + + our_gap_->setParent(track_->parent()); + track_->ReplaceBlock(block_, our_gap_); + + if (!position_command_) { + position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, our_gap_->index(), track_->Blocks().size(), true); + } + position_command_->redo(); + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); + + } else { + // Block is at the end of the track, simply remove it + + // Determine if it's proceeded by a gap, and remove that gap if so + Block* preceding = block_->previous(); + if (dynamic_cast(preceding)) { + track_->RippleRemoveBlock(preceding); + preceding->setParent(&memory_manager_); + + existing_merged_gap_ = static_cast(preceding); + } + + // Remove block in question + track_->RippleRemoveBlock(block_); + } +} + +void TrackReplaceBlockWithGapCommand::undo() +{ + if (our_gap_ || existing_gap_) { + track_->BeginOperation(); + + if (our_gap_) { + + // We made this gap, simply swap our gap back + track_->ReplaceBlock(our_gap_, block_); + our_gap_->setParent(&memory_manager_); + + position_command_->undo(); + + } else { + + // If we're here, assume that we extended an existing gap + rational original_gap_length = existing_gap_->length() - block_->length(); + + // If we merged two gaps together, restore the second one now + if (existing_merged_gap_) { + original_gap_length -= existing_merged_gap_->length(); + existing_merged_gap_->setParent(track_->parent()); + track_->InsertBlockAfter(existing_merged_gap_, existing_gap_); + existing_merged_gap_ = nullptr; + } + + // Restore original block + if (existing_gap_precedes_) { + track_->InsertBlockAfter(block_, existing_gap_); + } else { + track_->InsertBlockBefore(block_, existing_gap_); + } + + // Restore gap's original length + existing_gap_->set_length_and_media_out(original_gap_length); + + existing_gap_ = nullptr; + + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); + } else { + + // Our gap and existing gap were both null, our block must have been at the end and thus + // required no gap extension/replacement + + // However, we may have removed an unnecessary gap that preceded it + if (existing_merged_gap_) { + existing_merged_gap_->setParent(track_->parent()); + track_->AppendBlock(existing_merged_gap_); + existing_merged_gap_ = nullptr; + } + + // Restore block + track_->AppendBlock(block_); + + } + + for (auto it=transition_remove_commands_.crbegin(); it!=transition_remove_commands_.crend(); it++) { + (*it)->undo(); + } +} + +void TrackReplaceBlockWithGapCommand::CreateRemoveTransitionCommandIfNecessary(bool next) +{ + Block* relevant_block; + + if (next) { + relevant_block = block_->next(); + } else { + relevant_block = block_->previous(); + } + + TransitionBlock* transition_cast_test = dynamic_cast(relevant_block); + + if (transition_cast_test) { + if ((next && transition_cast_test->connected_out_block() == block_ && !transition_cast_test->connected_in_block()) + || (!next && transition_cast_test->connected_in_block() == block_ && !transition_cast_test->connected_out_block())) { + TransitionRemoveCommand* command = new TransitionRemoveCommand(transition_cast_test, true); + transition_remove_commands_.append(command); + } + } +} + } diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h index b7ceeb5f0..de868f2d6 100644 --- a/app/widget/timelinewidget/timelineundo.h +++ b/app/widget/timelinewidget/timelineundo.h @@ -1484,6 +1484,114 @@ private: }; +class TransitionRemoveCommand : public UndoCommand { +public: + TransitionRemoveCommand(TransitionBlock* block, bool remove_from_graph) : + block_(block), + remove_from_graph_(remove_from_graph), + remove_command_(nullptr) + { + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void redo() override + { + track_ = block_->track(); + out_block_ = block_->connected_out_block(); + in_block_ = block_->connected_in_block(); + + Q_ASSERT(out_block_ || in_block_); + + track_->BeginOperation(); + + TimeRange invalidate_range(block_->in(), block_->out()); + + if (in_block_) { + in_block_->set_length_and_media_in(in_block_->length() + block_->in_offset()); + } + + if (out_block_) { + out_block_->set_length_and_media_out(out_block_->length() + block_->out_offset()); + } + + if (in_block_) { + Node::DisconnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput)); + } + + if (out_block_) { + Node::DisconnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput)); + } + + track_->RippleRemoveBlock(block_); + + track_->EndOperation(); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); + + if (remove_from_graph_) { + if (!remove_command_) { + remove_command_ = CreateRemoveCommand(block_); + } + + remove_command_->redo(); + } + } + + virtual void undo() override + { + if (remove_from_graph_) { + remove_command_->undo(); + } + + track_->BeginOperation(); + + if (in_block_) { + track_->InsertBlockBefore(block_, in_block_); + } else { + track_->InsertBlockAfter(block_, out_block_); + } + + if (in_block_) { + Node::ConnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput)); + } + + if (out_block_) { + Node::ConnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput)); + } + + // These if statements must be separated because in_offset and out_offset report different things + // if only one block is connected vs two. So we have to connect the blocks first before we have + // an accurate return value from these offset functions. + if (in_block_) { + in_block_->set_length_and_media_in(in_block_->length() - block_->in_offset()); + } + + if (out_block_) { + out_block_->set_length_and_media_out(out_block_->length() - block_->out_offset()); + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); + } + +private: + TransitionBlock* block_; + + Track* track_; + + Block* out_block_; + Block* in_block_; + + bool remove_from_graph_; + UndoCommand* remove_command_; + +}; + class TrackReplaceBlockWithGapCommand : public UndoCommand { public: TrackReplaceBlockWithGapCommand(Track* track, Block* block) : @@ -1506,145 +1614,13 @@ public: return block_->project(); } - virtual void redo() override - { - if (block_->next()) { - track_->BeginOperation(); + virtual void redo() override; - // Invalidate the range inhabited by this block - TimeRange invalidate_range(block_->in(), block_->out()); - - // Block has a next, which means it's NOT at the end of the sequence and thus requires a gap - rational new_gap_length = block_->length(); - - Block* previous = block_->previous(); - Block* next = block_->next(); - - bool previous_is_a_gap = dynamic_cast(previous); - bool next_is_a_gap = dynamic_cast(next); - - if (previous_is_a_gap && next_is_a_gap) { - // Clip is preceded and followed by a gap, so we'll merge the two - existing_gap_ = static_cast(previous); - - existing_merged_gap_ = static_cast(next); - new_gap_length += existing_merged_gap_->length(); - track_->RippleRemoveBlock(existing_merged_gap_); - existing_merged_gap_->setParent(&memory_manager_); - } else if (previous_is_a_gap) { - // Extend this gap to fill space left by block - existing_gap_ = static_cast(previous); - } else if (next_is_a_gap) { - // Extend this gap to fill space left by block - existing_gap_ = static_cast(next); - } - - if (existing_gap_) { - // Extend an existing gap - new_gap_length += existing_gap_->length(); - existing_gap_->set_length_and_media_out(new_gap_length); - track_->RippleRemoveBlock(block_); - - existing_gap_precedes_ = (existing_gap_ == previous); - } else { - // No gap exists to fill this space, create a new one and swap it in - if (!our_gap_) { - our_gap_ = new GapBlock(); - our_gap_->set_length_and_media_out(new_gap_length); - } - - our_gap_->setParent(track_->parent()); - track_->ReplaceBlock(block_, our_gap_); - - if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, our_gap_->index(), track_->Blocks().size(), true); - } - position_command_->redo(); - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); - - } else { - // Block is at the end of the track, simply remove it - - // Determine if it's proceeded by a gap, and remove that gap if so - Block* preceding = block_->previous(); - if (dynamic_cast(preceding)) { - track_->RippleRemoveBlock(preceding); - preceding->setParent(&memory_manager_); - - existing_merged_gap_ = static_cast(preceding); - } - - // Remove block in question - track_->RippleRemoveBlock(block_); - } - } - - virtual void undo() override - { - if (our_gap_ || existing_gap_) { - track_->BeginOperation(); - - if (our_gap_) { - - // We made this gap, simply swap our gap back - track_->ReplaceBlock(our_gap_, block_); - our_gap_->setParent(&memory_manager_); - - position_command_->undo(); - - } else { - - // If we're here, assume that we extended an existing gap - rational original_gap_length = existing_gap_->length() - block_->length(); - - // If we merged two gaps together, restore the second one now - if (existing_merged_gap_) { - original_gap_length -= existing_merged_gap_->length(); - existing_merged_gap_->setParent(track_->parent()); - track_->InsertBlockAfter(existing_merged_gap_, existing_gap_); - existing_merged_gap_ = nullptr; - } - - // Restore original block - if (existing_gap_precedes_) { - track_->InsertBlockAfter(block_, existing_gap_); - } else { - track_->InsertBlockBefore(block_, existing_gap_); - } - - // Restore gap's original length - existing_gap_->set_length_and_media_out(original_gap_length); - - existing_gap_ = nullptr; - - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); - } else { - - // Our gap and existing gap were both null, our block must have been at the end and thus - // required no gap extension/replacement - - // However, we may have removed an unnecessary gap that preceded it - if (existing_merged_gap_) { - existing_merged_gap_->setParent(track_->parent()); - track_->AppendBlock(existing_merged_gap_); - existing_merged_gap_ = nullptr; - } - - // Restore block - track_->AppendBlock(block_); - - } - } + virtual void undo() override; private: + void CreateRemoveTransitionCommandIfNecessary(bool next); + Track* track_; Block* block_; @@ -1657,6 +1633,8 @@ private: QObject memory_manager_; + QVector transition_remove_commands_; + }; class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { @@ -2193,97 +2171,6 @@ private: }; -class TransitionRemoveCommand : public UndoCommand { -public: - TransitionRemoveCommand(TransitionBlock* block) : - block_(block) - { - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - track_ = block_->track(); - out_block_ = block_->connected_out_block(); - in_block_ = block_->connected_in_block(); - - Q_ASSERT(out_block_ || in_block_); - - track_->BeginOperation(); - - TimeRange invalidate_range(block_->in(), block_->out()); - - if (in_block_) { - in_block_->set_length_and_media_in(in_block_->length() + block_->in_offset()); - } - - if (out_block_) { - out_block_->set_length_and_media_out(out_block_->length() + block_->out_offset()); - } - - if (in_block_) { - Node::DisconnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput)); - } - - if (out_block_) { - Node::DisconnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput)); - } - - track_->RippleRemoveBlock(block_); - - track_->EndOperation(); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); - } - - virtual void undo() override - { - track_->BeginOperation(); - - if (in_block_) { - track_->InsertBlockBefore(block_, in_block_); - } else { - track_->InsertBlockAfter(block_, out_block_); - } - - if (in_block_) { - Node::ConnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput)); - } - - if (out_block_) { - Node::ConnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput)); - } - - // These if statements must be separated because in_offset and out_offset report different things - // if only one block is connected vs two. So we have to connect the blocks first before we have - // an accurate return value from these offset functions. - if (in_block_) { - in_block_->set_length_and_media_in(in_block_->length() - block_->in_offset()); - } - - if (out_block_) { - out_block_->set_length_and_media_out(out_block_->length() - block_->out_offset()); - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); - } - -private: - TransitionBlock* block_; - - Track* track_; - - Block* out_block_; - Block* in_block_; - -}; - } #endif // TIMELINEUNDOABLE_H diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 3529edb44..065c731a7 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -479,9 +479,7 @@ void TimelineWidget::DeleteSelected(bool ripple) // For transitions, remove them but extend their attached blocks to fill their place foreach (TransitionBlock* transition, transitions_to_delete) { - command->add_child(new TransitionRemoveCommand(transition)); - - command->add_child(new NodeRemoveWithExclusiveDependenciesAndDisconnect(transition)); + command->add_child(new TransitionRemoveCommand(transition, true)); } // Replace clips with gaps (effectively deleting them) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 926af5116..ec2b70641 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,14 +16,14 @@ function(olive_add_test GROUP NAME SOURCE) file(READ "${SOURCE}" TEST_FILE_CONTENT) - string(REGEX MATCHALL "OLIVE_ADD_TEST\(.[A-Za-z0-9]+\)" TEST_FUNCTIONS ${TEST_FILE_CONTENT}) + string(REGEX MATCHALL "OLIVE_ADD_TEST\(.[A-Za-z0-9_]+\)" TEST_FUNCTIONS ${TEST_FILE_CONTENT}) set(TEST_BODY "int main(int argc, char** argv)\n{\n") set(TEST_INDEX 1) list(LENGTH TEST_FUNCTIONS TEST_COUNT) foreach (TEST_FUNC ${TEST_FUNCTIONS}) string(REPLACE "OLIVE_ADD_TEST(" "" TEST_FUNC "${TEST_FUNC}") - string(APPEND TEST_BODY " std::cout << \"[${TEST_INDEX}/${TEST_COUNT}] ${GROUP} - ${TEST_FUNC}\" << std::endl;\n") - string(APPEND TEST_BODY " if (!olive::Test${TEST_FUNC}()) return 1;\n") + string(APPEND TEST_BODY " std::cout << \"[${TEST_INDEX}/${TEST_COUNT}] ${GROUP} - ${TEST_FUNC}\";\n") + string(APPEND TEST_BODY " if (olive::Test${TEST_FUNC}()) {std::cout << \" - PASSED\" << std::endl;}else{std::cout << \" - FAILED\" << std::endl;return 1;\n}\n") MATH(EXPR TEST_INDEX "${TEST_INDEX}+1") endforeach() string(APPEND TEST_BODY " return 0;\n}") diff --git a/tests/timeline/timeline-tests.cpp b/tests/timeline/timeline-tests.cpp index ef85fdada..7c68284f2 100644 --- a/tests/timeline/timeline-tests.cpp +++ b/tests/timeline/timeline-tests.cpp @@ -20,6 +20,7 @@ #include "core.h" #include "node/block/clip/clip.h" +#include "node/block/transition/crossdissolve/crossdissolvetransition.h" #include "node/math/math/math.h" #include "node/math/merge/merge.h" #include "node/project/project.h" @@ -119,12 +120,12 @@ OLIVE_ADD_TEST(Trim) ClipBlock* block1 = new ClipBlock(); block1->set_length_and_media_out(2); - project.setParent(block1); + block1->setParent(&project); track->AppendBlock(block1); ClipBlock* block2 = new ClipBlock(); block2->set_length_and_media_out(2); - project.setParent(block2); + block2->setParent(&project); track->AppendBlock(block2); // There should be two blocks right now @@ -216,4 +217,275 @@ OLIVE_ADD_TEST(Trim) OLIVE_TEST_END; } +OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsOnly) +{ + TIMELINE_TEST_START; + + // Create a track that goes clip -> clip -> clip + sequence.add_default_nodes(); + Track* track = sequence.track_list(Track::kVideo)->GetTracks().first(); + + ClipBlock* a = new ClipBlock(); + a->setParent(&project); + track->AppendBlock(a); + + ClipBlock* b = new ClipBlock(); + b->setParent(&project); + track->AppendBlock(b); + + ClipBlock* c = new ClipBlock(); + c->setParent(&project); + track->AppendBlock(c); + + { + // Replace clip C with a gap + TrackReplaceBlockWithGapCommand command(track, c); + command.redo(); + + // Clip should be removed without any gap actually taking its place, since the clip is at the + // end of the track + OLIVE_ASSERT(track->Blocks().size() == 2); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + + command.undo(); + + OLIVE_ASSERT(track->Blocks().size() == 3); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + OLIVE_ASSERT(track->Blocks().at(2) == c); + } + + { + // Replace clip B with a gap + TrackReplaceBlockWithGapCommand command(track, b); + command.redo(); + + // B should be replaced with a gap + OLIVE_ASSERT(track->Blocks().size() == 3); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) != b); + OLIVE_ASSERT(dynamic_cast(track->Blocks().at(1))); + OLIVE_ASSERT(track->Blocks().at(1)->length() == b->length()); + OLIVE_ASSERT(track->Blocks().at(2) == c); + + command.undo(); + + OLIVE_ASSERT(track->Blocks().size() == 3); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + OLIVE_ASSERT(track->Blocks().at(2) == c); + } + + OLIVE_TEST_END; +} + +OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) +{ + TIMELINE_TEST_START; + + // Create a track that goes clip -> gap -> clip -> clip -> gap -> clip + sequence.add_default_nodes(); + Track* track = sequence.track_list(Track::kVideo)->GetTracks().first(); + + ClipBlock* a = new ClipBlock(); + a->setParent(&project); + track->AppendBlock(a); + + GapBlock* b = new GapBlock(); + b->setParent(&project); + track->AppendBlock(b); + + ClipBlock* c = new ClipBlock(); + c->setParent(&project); + track->AppendBlock(c); + + GapBlock* d = new GapBlock(); + d->setParent(&project); + track->AppendBlock(d); + + ClipBlock* e = new ClipBlock(); + e->setParent(&project); + track->AppendBlock(e); + + { + // Replace clip E with a gap + TrackReplaceBlockWithGapCommand command(track, e); + command.redo(); + + // Both clips D and E should be removed because this command should remove any trailing gaps + OLIVE_ASSERT(track->Blocks().size() == 3); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + OLIVE_ASSERT(track->Blocks().at(2) == c); + + // Test undo + command.undo(); + + OLIVE_ASSERT(track->Blocks().size() == 5); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + OLIVE_ASSERT(track->Blocks().at(2) == c); + OLIVE_ASSERT(track->Blocks().at(3) == d); + OLIVE_ASSERT(track->Blocks().at(4) == e); + } + + { + // Replace clip A with a gap + rational original_length_of_a = a->length(); + rational original_length_of_b = b->length(); + + TrackReplaceBlockWithGapCommand command(track, a); + command.redo(); + + // A should be removed and B should take its place + OLIVE_ASSERT(track->Blocks().size() == 4); + + OLIVE_ASSERT(track->Blocks().at(0) == b); + OLIVE_ASSERT(track->Blocks().at(1) == c); + OLIVE_ASSERT(track->Blocks().at(2) == d); + OLIVE_ASSERT(track->Blocks().at(3) == e); + OLIVE_ASSERT(b->length() == original_length_of_a + original_length_of_b); + + // Test undo + command.undo(); + + OLIVE_ASSERT(track->Blocks().size() == 5); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + OLIVE_ASSERT(track->Blocks().at(2) == c); + OLIVE_ASSERT(track->Blocks().at(3) == d); + OLIVE_ASSERT(track->Blocks().at(4) == e); + OLIVE_ASSERT(a->length() == original_length_of_a); + OLIVE_ASSERT(b->length() == original_length_of_b); + } + + { + // Replace clip C with a gap + rational original_length_of_b = b->length(); + rational original_length_of_c = c->length(); + rational original_length_of_d = d->length(); + + TrackReplaceBlockWithGapCommand command(track, c); + command.redo(); + + // C and D should be removed, and B should take both of their places + OLIVE_ASSERT(track->Blocks().size() == 3); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + OLIVE_ASSERT(track->Blocks().at(2) == e); + OLIVE_ASSERT(b->length() == original_length_of_b + original_length_of_c + original_length_of_d); + + // Test undo + command.undo(); + + OLIVE_ASSERT(track->Blocks().size() == 5); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + OLIVE_ASSERT(track->Blocks().at(2) == c); + OLIVE_ASSERT(track->Blocks().at(3) == d); + OLIVE_ASSERT(track->Blocks().at(4) == e); + OLIVE_ASSERT(b->length() == original_length_of_b); + OLIVE_ASSERT(c->length() == original_length_of_c); + OLIVE_ASSERT(d->length() == original_length_of_d); + } + + { + // Add a fourth clip at the end of the track + ClipBlock* f = new ClipBlock(); + f->setParent(&project); + track->AppendBlock(f); + + // Try replacing E with a block again + TrackReplaceBlockWithGapCommand command(track, e); + rational original_length_of_d = d->length(); + rational original_length_of_e = e->length(); + command.redo(); + + // E should be removed and D should have taken its place + OLIVE_ASSERT(track->Blocks().size() == 5); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + OLIVE_ASSERT(track->Blocks().at(2) == c); + OLIVE_ASSERT(track->Blocks().at(3) == d); + OLIVE_ASSERT(track->Blocks().at(4) == f); + OLIVE_ASSERT(d->length() == original_length_of_d + original_length_of_e); + + command.undo(); + + OLIVE_ASSERT(track->Blocks().size() == 6); + OLIVE_ASSERT(track->Blocks().at(0) == a); + OLIVE_ASSERT(track->Blocks().at(1) == b); + OLIVE_ASSERT(track->Blocks().at(2) == c); + OLIVE_ASSERT(track->Blocks().at(3) == d); + OLIVE_ASSERT(track->Blocks().at(4) == e); + OLIVE_ASSERT(track->Blocks().at(5) == f); + OLIVE_ASSERT(d->length() == original_length_of_d); + OLIVE_ASSERT(e->length() == original_length_of_e); + } + + OLIVE_TEST_END; +} + +#define UsingTransition CrossDissolveTransition + +OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndTransitions) +{ + TIMELINE_TEST_START; + + // Create a track that goes clip -> gap -> clip -> clip -> gap -> clip + sequence.add_default_nodes(); + Track* track = sequence.track_list(Track::kVideo)->GetTracks().first(); + + UsingTransition* a_in = new UsingTransition(); + a_in->setParent(&project); + track->AppendBlock(a_in); + + ClipBlock* a = new ClipBlock(); + a->setParent(&project); + track->AppendBlock(a); + + UsingTransition* a_to_b = new UsingTransition(); + a_to_b->setParent(&project); + track->AppendBlock(a_to_b); + + ClipBlock* b = new ClipBlock(); + b->setParent(&project); + track->AppendBlock(b); + + UsingTransition* b_out = new UsingTransition(); + b_out->setParent(&project); + track->AppendBlock(b_out); + + Node::ConnectEdge(a, NodeInput(a_in, UsingTransition::kInBlockInput)); + Node::ConnectEdge(a, NodeInput(a_to_b, UsingTransition::kOutBlockInput)); + Node::ConnectEdge(b, NodeInput(a_to_b, UsingTransition::kInBlockInput)); + Node::ConnectEdge(b, NodeInput(b_out, UsingTransition::kOutBlockInput)); + + { + // Replace A with gap + TrackReplaceBlockWithGapCommand command(track, a); + command.redo(); + + // A should be replaced with a gap and so should A_IN since A was the only clip connected to it. + // Also A_TO_B should only be connected to B now + OLIVE_ASSERT(track->Blocks().size() == 4); + OLIVE_ASSERT(dynamic_cast(track->Blocks().at(0))); + OLIVE_ASSERT(track->Blocks().at(1) == a_to_b); + OLIVE_ASSERT(track->Blocks().at(2) == b); + OLIVE_ASSERT(track->Blocks().at(3) == b_out); + + command.undo(); + + OLIVE_ASSERT(track->Blocks().size() == 5); + OLIVE_ASSERT(track->Blocks().at(0) == a_in); + OLIVE_ASSERT(track->Blocks().at(1) == a); + OLIVE_ASSERT(track->Blocks().at(2) == a_to_b); + OLIVE_ASSERT(track->Blocks().at(3) == b); + OLIVE_ASSERT(track->Blocks().at(4) == b_out); + } + + OLIVE_TEST_END; +} + }