/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team Modifications Copyright (C) 2025 mikesolar This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #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.h" #include "node/project/sequence/sequence.h" #include "testutil.h" #include "timeline/timelineundogeneral.h" #include "timeline/timelineundopointer.h" #include "undo/undocommand.h" namespace olive { #define TIMELINE_TEST_START \ ColorManager::SetUpDefaultConfig(); \ Project project; \ Sequence sequence; \ sequence.setParent(&project) OLIVE_ADD_TEST(AddTrack) { TIMELINE_TEST_START; Track *first_video_track, *first_audio_track; { // Test creating initial video track first_video_track = TimelineAddTrackCommand::RunImmediately( sequence.track_list(Track::kVideo)); OLIVE_ASSERT(sequence.GetConnectedOutput(Sequence::kTextureInput) == first_video_track); OLIVE_ASSERT(sequence.track_list(Track::kVideo)->GetTrackCount() == 1); OLIVE_ASSERT(sequence.track_list(Track::kVideo)->GetTrackAt(0) == first_video_track); } { // Test creating initial audio track first_audio_track = TimelineAddTrackCommand::RunImmediately( sequence.track_list(Track::kAudio)); OLIVE_ASSERT(sequence.GetConnectedOutput(Sequence::kSamplesInput) == first_audio_track); OLIVE_ASSERT(sequence.track_list(Track::kAudio)->GetTrackCount() == 1); OLIVE_ASSERT(sequence.track_list(Track::kAudio)->GetTrackAt(0) == first_audio_track); } { // Test creating second video track with merge Track *second_video_track = TimelineAddTrackCommand::RunImmediately( sequence.track_list(Track::kVideo), true); OLIVE_ASSERT(sequence.GetConnectedOutput(Sequence::kTextureInput) != first_video_track); OLIVE_ASSERT(sequence.GetConnectedOutput(Sequence::kTextureInput) != second_video_track); OLIVE_ASSERT(sequence.track_list(Track::kVideo)->GetTrackCount() == 2); OLIVE_ASSERT(sequence.track_list(Track::kVideo)->GetTrackAt(1) == second_video_track); MergeNode *merge = dynamic_cast( sequence.GetConnectedOutput(Sequence::kTextureInput)); OLIVE_ASSERT(merge); OLIVE_ASSERT(merge->GetConnectedOutput(MergeNode::kBaseIn) == first_video_track); OLIVE_ASSERT(merge->GetConnectedOutput(MergeNode::kBlendIn) == second_video_track); } { // Test creating second audio track with merge Track *second_audio_track = TimelineAddTrackCommand::RunImmediately( sequence.track_list(Track::kAudio), true); OLIVE_ASSERT(sequence.GetConnectedOutput(Sequence::kSamplesInput) != first_audio_track); OLIVE_ASSERT(sequence.GetConnectedOutput(Sequence::kSamplesInput) != second_audio_track); OLIVE_ASSERT(sequence.track_list(Track::kAudio)->GetTrackCount() == 2); OLIVE_ASSERT(sequence.track_list(Track::kAudio)->GetTrackAt(1) == second_audio_track); MathNode *merge = dynamic_cast( sequence.GetConnectedOutput(Sequence::kSamplesInput)); OLIVE_ASSERT(merge); OLIVE_ASSERT(merge->GetConnectedOutput(MathNode::kParamAIn) == first_audio_track); OLIVE_ASSERT(merge->GetConnectedOutput(MathNode::kParamBIn) == second_audio_track); } OLIVE_TEST_END; } OLIVE_ADD_TEST(SequenceDefaults) { TIMELINE_TEST_START; sequence.add_default_nodes(); OLIVE_ASSERT(sequence.GetTracks().size() == 2); Track *tex_connect = dynamic_cast(sequence.GetConnectedTextureOutput()); OLIVE_ASSERT(tex_connect); Track *smp_connect = dynamic_cast(sequence.GetConnectedSampleOutput()); OLIVE_ASSERT(smp_connect); OLIVE_ASSERT(tex_connect != smp_connect); OLIVE_ASSERT(sequence.GetTracks().contains(tex_connect)); OLIVE_ASSERT(sequence.GetTracks().contains(smp_connect)); OLIVE_TEST_END; } OLIVE_ADD_TEST(Trim) { TIMELINE_TEST_START; sequence.add_default_nodes(); Track *track = sequence.GetTracks().first(); ClipBlock *block1 = new ClipBlock(); block1->set_length_and_media_out(2); block1->setParent(&project); track->AppendBlock(block1); ClipBlock *block2 = new ClipBlock(); block2->set_length_and_media_out(2); block2->setParent(&project); track->AppendBlock(block2); // There should be two blocks right now OLIVE_ASSERT(track->Blocks().size() == 2); { // Trim out point of second block BlockTrimCommand command(track, block2, 1, Timeline::kTrimOut); command.redo_now(); // No block should have been added OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 1); OLIVE_ASSERT(block1->length() == 2); command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); OLIVE_ASSERT(block1->length() == 2); } { // Trim in point of second block BlockTrimCommand command(track, block2, 1, Timeline::kTrimIn); command.redo_now(); // Gap should be inserted in between OLIVE_ASSERT(track->Blocks().size() == 3); GapBlock *gap = dynamic_cast(track->Blocks().at(1)); OLIVE_ASSERT(gap); OLIVE_ASSERT(gap->length() == 1); OLIVE_ASSERT(block2->length() == 1); OLIVE_ASSERT(block1->length() == 2); OLIVE_ASSERT(block1->next() == gap); OLIVE_ASSERT(block2->previous() == gap); command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); OLIVE_ASSERT(block1->length() == 2); } { // Trim out point of first block BlockTrimCommand command(track, block1, 1, Timeline::kTrimOut); command.redo_now(); // Gap should be inserted in between OLIVE_ASSERT(track->Blocks().size() == 3); GapBlock *gap = dynamic_cast(track->Blocks().at(1)); OLIVE_ASSERT(gap); OLIVE_ASSERT(gap->length() == 1); OLIVE_ASSERT(block1->length() == 1); OLIVE_ASSERT(block2->length() == 2); OLIVE_ASSERT(block1->next() == gap); OLIVE_ASSERT(block2->previous() == gap); command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); OLIVE_ASSERT(block1->length() == 2); } { // Trim in point of first block BlockTrimCommand command(track, block1, 1, Timeline::kTrimIn); command.redo_now(); // Gap should be prepended to the start OLIVE_ASSERT(track->Blocks().size() == 3); GapBlock *gap = dynamic_cast(track->Blocks().at(0)); OLIVE_ASSERT(gap); OLIVE_ASSERT(gap->length() == 1); OLIVE_ASSERT(block1->length() == 1); OLIVE_ASSERT(block2->length() == 2); OLIVE_ASSERT(block1->next() == block2); OLIVE_ASSERT(block1->previous() == gap); command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); OLIVE_ASSERT(block1->length() == 2); } 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_now(); // 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_now(); 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_now(); // 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_now(); 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_now(); // 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_now(); 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_now(); // 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_now(); 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_now(); // 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_now(); 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_now(); // 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_now(); 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_now(); // 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_now(); 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; } OLIVE_ADD_TEST(InsertGaps_SingleTrack) { TIMELINE_TEST_START; sequence.add_default_nodes(); TrackList *list = sequence.track_list(Track::kVideo); Track *track = list->GetTracks().first(); ClipBlock *a = new ClipBlock(); a->set_length_and_media_out(1); a->setParent(&project); track->AppendBlock(a); ClipBlock *b = new ClipBlock(); b->set_length_and_media_out(1); b->setParent(&project); track->AppendBlock(b); ClipBlock *c = new ClipBlock(); c->set_length_and_media_out(1); c->setParent(&project); track->AppendBlock(c); 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); { // Insert gap at the start of the track, all blocks should be unsplit and shifted to the right TrackListInsertGaps command(list, 0, 2); command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 4); OLIVE_ASSERT(dynamic_cast(track->Blocks().at(0))); OLIVE_ASSERT(track->Blocks().at(0)->length() == 2); OLIVE_ASSERT(track->Blocks().at(1) == a); OLIVE_ASSERT(track->Blocks().at(2) == b); OLIVE_ASSERT(track->Blocks().at(3) == c); command.undo_now(); 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); } { // Insert gap in the middle of block A, block A should be halved with a copy at 2 and the gap at 1 TrackListInsertGaps command(list, rational(1, 2), 2); command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a); OLIVE_ASSERT(track->Blocks().at(0)->length() == rational(1, 2)); OLIVE_ASSERT(dynamic_cast(track->Blocks().at(1))); OLIVE_ASSERT(dynamic_cast(track->Blocks().at(2))); OLIVE_ASSERT(track->Blocks().at(3) == b); OLIVE_ASSERT(track->Blocks().at(4) == c); command.undo_now(); OLIVE_ASSERT_EQUAL(track->Blocks().size(), 3); OLIVE_ASSERT_EQUAL(track->Blocks().at(0), a); OLIVE_ASSERT_EQUAL(track->Blocks().at(0)->length(), 1); OLIVE_ASSERT_EQUAL(track->Blocks().at(1), b); OLIVE_ASSERT_EQUAL(track->Blocks().at(2), c); } { // Insert gap between block A and B, blocks should be unsplit with a gap at 1 TrackListInsertGaps command(list, 1, 2); command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 4); OLIVE_ASSERT(track->Blocks().at(0) == a); OLIVE_ASSERT(dynamic_cast(track->Blocks().at(1))); OLIVE_ASSERT(track->Blocks().at(2) == b); OLIVE_ASSERT(track->Blocks().at(3) == c); command.undo_now(); 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); } { // Insert gap at end, nothing should be added TrackListInsertGaps command(list, 3, 2); command.redo_now(); 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); command.undo_now(); 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; } }