/*** Olive - Non-Linear Video Editor Copyright (C) 2019 Olive Team 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 "undo.h" #include "core.h" #include "node/block/clip/clip.h" #include "node/block/transition/transition.h" #include "node/graph.h" #include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER Node* TakeNodeFromParentGraph(Node* n, QObject* new_parent = nullptr) { static_cast(n->parent())->TakeNode(n, new_parent); return n; } BlockResizeCommand::BlockResizeCommand(Block *block, rational new_length, QUndoCommand* parent) : UndoCommand(parent), block_(block), old_length_(block->length()), new_length_(new_length) { } Project *BlockResizeCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void BlockResizeCommand::redo_internal() { block_->set_length_and_media_out(new_length_); } void BlockResizeCommand::undo_internal() { block_->set_length_and_media_out(old_length_); } BlockResizeWithMediaInCommand::BlockResizeWithMediaInCommand(Block *block, rational new_length, QUndoCommand *parent) : UndoCommand(parent), block_(block), old_length_(block->length()), new_length_(new_length) { } Project *BlockResizeWithMediaInCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void BlockResizeWithMediaInCommand::redo_internal() { block_->set_length_and_media_in(new_length_); } void BlockResizeWithMediaInCommand::undo_internal() { block_->set_length_and_media_in(old_length_); } BlockSetMediaInCommand::BlockSetMediaInCommand(Block *block, rational new_media_in, QUndoCommand* parent) : UndoCommand(parent), block_(block), old_media_in_(block->media_in()), new_media_in_(new_media_in) { } Project *BlockSetMediaInCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void BlockSetMediaInCommand::redo_internal() { block_->set_media_in(new_media_in_); } void BlockSetMediaInCommand::undo_internal() { block_->set_media_in(old_media_in_); } TrackRippleRemoveBlockCommand::TrackRippleRemoveBlockCommand(TrackOutput *track, Block *block, QUndoCommand *parent) : UndoCommand(parent), track_(track), block_(block) { } Project *TrackRippleRemoveBlockCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void TrackRippleRemoveBlockCommand::redo_internal() { before_ = block_->previous(); track_->RippleRemoveBlock(block_); } void TrackRippleRemoveBlockCommand::undo_internal() { if (before_) { track_->InsertBlockAfter(block_, before_); } else { track_->PrependBlock(block_); } } TrackInsertBlockAfterCommand::TrackInsertBlockAfterCommand(TrackOutput *track, Block *block, Block *before, QUndoCommand *parent) : UndoCommand(parent), track_(track), block_(block), before_(before) { } Project *TrackInsertBlockAfterCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void TrackInsertBlockAfterCommand::redo_internal() { track_->InsertBlockAfter(block_, before_); } void TrackInsertBlockAfterCommand::undo_internal() { track_->RippleRemoveBlock(block_); } TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand(TrackOutput *track, rational in, rational out, QUndoCommand *parent) : UndoCommand(parent), track_(track), in_(in), out_(out), splice_(false), trim_out_(nullptr), trim_in_(nullptr), insert_(nullptr) { } Project *TrackRippleRemoveAreaCommand::GetRelevantProject() const { return static_cast(track_->parent())->project(); } void TrackRippleRemoveAreaCommand::SetInsert(Block *insert) { insert_ = insert; } void TrackRippleRemoveAreaCommand::redo_internal() { // Iterate through blocks determining which need trimming/removing/splitting foreach (Block* block, track_->Blocks()) { if (block->in() < in_ && block->out() > out_) { // The area entirely within this Block trim_out_ = block; splice_ = true; // We don't need to do anything else here break; } else if (block->in() >= in_ && block->out() <= out_) { // This Block's is entirely within the area removed_blocks_.append(block); } else if (block->in() < in_ && block->out() >= in_) { // This Block's out point exceeds `in` trim_out_ = block; } else if (block->in() <= out_ && block->out() > out_) { // This Block's in point exceeds `out` trim_in_ = block; } } track_->BeginOperation(); // If we picked up a block to splice if (splice_) { // Split the block here trim_in_ = static_cast(trim_out_->copy()); static_cast(track_->parent())->AddNode(trim_in_); Node::CopyInputs(trim_out_, trim_in_); trim_out_old_length_ = trim_out_->length(); trim_out_->set_length_and_media_out(in_ - trim_out_->in()); trim_in_->set_length_and_media_in(trim_out_old_length_ - (out_ - trim_out_->in())); track_->InsertBlockAfter(trim_in_, trim_out_); } else { // If we picked up a block to trim the in point of if (trim_in_) { trim_in_old_length_ = trim_in_->length(); trim_in_new_length_ = trim_in_->out() - out_; } // If we picked up a block to trim the out point of if (trim_out_) { trim_out_old_length_ = trim_out_->length(); trim_out_new_length_ = in_ - trim_out_->in(); } // If we picked up a block to trim the in point of if (trim_in_old_length_ != trim_in_new_length_) { trim_in_->set_length_and_media_in(trim_in_new_length_); } // Remove all blocks that are flagged for removal foreach (Block* remove_block, removed_blocks_) { track_->RippleRemoveBlock(remove_block); BlockUnlinkAllCommand* unlink_command = new BlockUnlinkAllCommand(remove_block); unlink_command->redo(); remove_block_commands_.append(unlink_command); NodeRemoveWithExclusiveDeps* remove_command = new NodeRemoveWithExclusiveDeps(static_cast(remove_block->parent()), remove_block); remove_command->redo(); remove_block_commands_.append(remove_command); } // If we picked up a block to trim the out point of if (trim_out_old_length_ != trim_out_new_length_) { trim_out_->set_length_and_media_out(trim_out_new_length_); } } // If we were given a block to insert, insert it here if (insert_) { if (!trim_out_) { // This is the start of the Sequence track_->PrependBlock(insert_); } else if (!trim_in_) { // This is the end of the Sequence track_->AppendBlock(insert_); } else { // This is somewhere in the middle of the Sequence track_->InsertBlockAfter(insert_, trim_out_); } } track_->EndOperation(); track_->InvalidateCache(TimeRange(in_, insert_ ? out_ : RATIONAL_MAX), track_->block_input(), track_->block_input()); } void TrackRippleRemoveAreaCommand::undo_internal() { track_->BeginOperation(); // If we were given a block to insert, insert it here if (insert_ != nullptr) { track_->RippleRemoveBlock(insert_); } if (splice_) { // trim_in_ is our copy and trim_out_ is our original track_->RippleRemoveBlock(trim_in_); trim_out_->set_length_and_media_out(trim_out_old_length_); delete TakeNodeFromParentGraph(trim_in_); } else { // If we picked up a block to trim the out point of if (trim_out_old_length_ != trim_out_new_length_) { trim_out_->set_length_and_media_out(trim_out_old_length_); } // Restore blocks that were removed for (int i=remove_block_commands_.size()-1;i>=0;i--) { UndoCommand* command = remove_block_commands_.at(i); command->undo(); delete command; } remove_block_commands_.clear(); for (int i=removed_blocks_.size()-1;i>=0;i--) { Block* remove_block = removed_blocks_.at(i); if (trim_in_) { track_->InsertBlockBefore(remove_block, trim_in_); } else { track_->AppendBlock(remove_block); } } removed_blocks_.clear(); // If we picked up a block to trim the in point of if (trim_in_old_length_ != trim_in_new_length_) { trim_in_->set_length_and_media_in(trim_in_old_length_); } } track_->EndOperation(); track_->InvalidateCache(TimeRange(in_, insert_ ? out_ : RATIONAL_MAX), track_->block_input(), track_->block_input()); } TrackPlaceBlockCommand::TrackPlaceBlockCommand(TrackList *timeline, int track, Block *block, rational in, QUndoCommand *parent) : TrackRippleRemoveAreaCommand(nullptr, in, 0, parent), // Out gets set correctly in redo() timeline_(timeline), track_index_(track), gap_(nullptr) { insert_ = block; } Project *TrackPlaceBlockCommand::GetRelevantProject() const { return static_cast(static_cast(timeline_->parent())->parent())->project(); } void TrackPlaceBlockCommand::redo_internal() { added_track_count_ = 0; // Get track (or make it if necessary) while (track_index_ >= timeline_->GetTracks().size()) { timeline_->AddTrack(); added_track_count_++; } track_ = timeline_->GetTrackAt(track_index_); append_ = (in_ >= track_->track_length()); // Check if the placement location is past the end of the timeline if (append_) { if (in_ > track_->track_length()) { // If so, insert a gap here gap_ = new GapBlock(); gap_->set_length_and_media_out(in_ - track_->track_length()); static_cast(track_->parent())->AddNode(gap_); track_->AppendBlock(gap_); } track_->AppendBlock(insert_); } else { out_ = in_ + insert_->length(); // Place the Block at this point TrackRippleRemoveAreaCommand::redo_internal(); } } void TrackPlaceBlockCommand::undo_internal() { if (append_) { track_->RippleRemoveBlock(insert_); if (gap_ != nullptr) { track_->RippleRemoveBlock(gap_); delete TakeNodeFromParentGraph(gap_); } } else { TrackRippleRemoveAreaCommand::undo_internal(); } for (;added_track_count_>0;added_track_count_--) { timeline_->RemoveTrack(); } } BlockSplitCommand::BlockSplitCommand(TrackOutput* track, Block *block, rational point, QUndoCommand *parent) : UndoCommand(parent), track_(track), block_(block), new_length_(point - block->in()), old_length_(block->length()), point_(point) { Q_ASSERT(point > block_->in() && point < block_->out() && block_->type() == Block::kClip); // Ensures that this block is deleted if this action is undone new_block_ = static_cast(block_->copy()); new_block_->setParent(&memory_manager_); // Determine if the block outputs to an "out" transition TransitionBlock* transition = TransitionBlock::GetBlockOutTransition(block_); if (transition) { transitions_to_move_.append(transition->out_block_input()); } } Project *BlockSplitCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void BlockSplitCommand::redo_internal() { track_->BeginOperation(); static_cast(block_->parent())->AddNode(new_block_); Node::CopyInputs(block_, new_block_); rational new_part_length = block_->length() - (point_ - block_->in()); block_->set_length_and_media_out(new_length_); new_block_->set_length_and_media_in(new_part_length); track_->InsertBlockAfter(new_block_, block_); foreach (NodeInput* transition, transitions_to_move_) { NodeParam::DisconnectEdge(block_->output(), transition); NodeParam::ConnectEdge(new_block_->output(), transition); } track_->EndOperation(); } void BlockSplitCommand::undo_internal() { track_->BeginOperation(); block_->set_length_and_media_out(old_length_); track_->RippleRemoveBlock(new_block_); TakeNodeFromParentGraph(new_block_, &memory_manager_); foreach (NodeInput* transition, transitions_to_move_) { NodeParam::DisconnectEdge(new_block_->output(), transition); NodeParam::ConnectEdge(block_->output(), transition); } track_->EndOperation(); } Block *BlockSplitCommand::new_block() { return new_block_; } TrackSplitAtTimeCommand::TrackSplitAtTimeCommand(TrackOutput *track, rational point, QUndoCommand *parent) : UndoCommand(parent), track_(track) { // Find Block that contains this time foreach (Block* b, track->Blocks()) { if (b->out() == point) { // This time is between blocks, no split needs to occur return; } else if (b->in() < point && b->out() > point) { // We found the Block, split it new BlockSplitCommand(track_, b, point, this); return; } } } Project *TrackSplitAtTimeCommand::GetRelevantProject() const { return static_cast(track_->parent())->project(); } TrackReplaceBlockCommand::TrackReplaceBlockCommand(TrackOutput* track, Block *old, Block *replace, QUndoCommand *parent) : UndoCommand(parent), track_(track), old_(old), replace_(replace) { } Project *TrackReplaceBlockCommand::GetRelevantProject() const { return static_cast(track_->parent())->project(); } void TrackReplaceBlockCommand::redo_internal() { track_->ReplaceBlock(old_, replace_); } void TrackReplaceBlockCommand::undo_internal() { track_->ReplaceBlock(replace_, old_); } TrackPrependBlockCommand::TrackPrependBlockCommand(TrackOutput *track, Block *block, QUndoCommand *parent) : UndoCommand(parent), track_(track), block_(block) { } Project *TrackPrependBlockCommand::GetRelevantProject() const { return static_cast(track_->parent())->project(); } void TrackPrependBlockCommand::redo_internal() { track_->PrependBlock(block_); } void TrackPrependBlockCommand::undo_internal() { track_->RippleRemoveBlock(block_); } BlockSplitPreservingLinksCommand::BlockSplitPreservingLinksCommand(const QVector &blocks, const QList ×, QUndoCommand *parent) : UndoCommand(parent), blocks_(blocks), times_(times) { QVector< QVector > split_blocks(times.size()); for (int i=0;i splits(blocks.size()); for (int j=0;jin() < time && b->out() > time) { TrackOutput* track = TrackOutput::TrackFromBlock(b); Q_ASSERT(track); BlockSplitCommand* split_command = new BlockSplitCommand(track, b, time, this); splits.replace(j, split_command->new_block()); } else { splits.replace(j, nullptr); } } split_blocks.replace(i, splits); } // Now that we've determined all the splits, we can relink everything for (int i=0;i& split_list, split_blocks) { Block::Link(split_list.at(i), split_list.at(j)); } } } } } Project *BlockSplitPreservingLinksCommand::GetRelevantProject() const { return static_cast(blocks_.first()->parent())->project(); } TimelineRippleDeleteGapsAtRegionsCommand::TimelineRippleDeleteGapsAtRegionsCommand(ViewerOutput *vo, const TimeRangeList ®ions, QUndoCommand *parent) : UndoCommand(parent), timeline_(vo), regions_(regions) { } Project *TimelineRippleDeleteGapsAtRegionsCommand::GetRelevantProject() const { return static_cast(timeline_->parent())->project(); } void TimelineRippleDeleteGapsAtRegionsCommand::redo_internal() { foreach (const TimeRange& range, regions_) { rational max_ripple_length = range.length(); QList blocks_around_range; foreach (TrackOutput* track, timeline_->GetTracks()) { // Get the block from every other track that is either at or just before our block's in point Block* block_at_time = track->NearestBlockBeforeOrAt(range.in()); if (block_at_time) { if (block_at_time->type() == Block::kGap) { max_ripple_length = qMin(block_at_time->length(), max_ripple_length); } else { max_ripple_length = 0; break; } blocks_around_range.append(block_at_time); } } if (max_ripple_length > 0) { foreach (Block* resize, blocks_around_range) { if (resize->length() == max_ripple_length) { // Remove block entirely TrackRippleRemoveBlockCommand* remove_command = new TrackRippleRemoveBlockCommand(TrackOutput::TrackFromBlock(resize), resize); remove_command->redo(); commands_.append(remove_command); } else { // Resize block BlockResizeCommand* resize_command = new BlockResizeCommand(resize, resize->length() - max_ripple_length); resize_command->redo(); commands_.append(resize_command); } } } } } void TimelineRippleDeleteGapsAtRegionsCommand::undo_internal() { for (int i=commands_.size()-1;i>=0;i--) { commands_.at(i)->undo(); delete commands_.at(i); } commands_.empty(); } WorkareaSetEnabledCommand::WorkareaSetEnabledCommand(Project* project, TimelinePoints *points, bool enabled, QUndoCommand *parent) : UndoCommand(parent), project_(project), points_(points), old_enabled_(points_->workarea()->enabled()), new_enabled_(enabled) { } Project *WorkareaSetEnabledCommand::GetRelevantProject() const { return project_; } void WorkareaSetEnabledCommand::redo_internal() { points_->workarea()->set_enabled(new_enabled_); } void WorkareaSetEnabledCommand::undo_internal() { points_->workarea()->set_enabled(old_enabled_); } WorkareaSetRangeCommand::WorkareaSetRangeCommand(Project* project, TimelinePoints *points, const TimeRange &range, QUndoCommand *parent) : UndoCommand(parent), project_(project), points_(points), old_range_(points_->workarea()->range()), new_range_(range) { } Project *WorkareaSetRangeCommand::GetRelevantProject() const { return project_; } void WorkareaSetRangeCommand::redo_internal() { points_->workarea()->set_range(new_range_); } void WorkareaSetRangeCommand::undo_internal() { points_->workarea()->set_range(old_range_); } BlockLinkCommand::BlockLinkCommand(Block *a, Block *b, bool link, QUndoCommand *parent) : UndoCommand(parent), a_(a), b_(b), link_(link) { } Project *BlockLinkCommand::GetRelevantProject() const { return static_cast(a_->parent())->project(); } void BlockLinkCommand::redo_internal() { if (link_) { done_ = Block::Link(a_, b_); } else { done_ = Block::Unlink(a_, b_); } } void BlockLinkCommand::undo_internal() { if (done_) { if (link_) { Block::Unlink(a_, b_); } else { Block::Link(a_, b_); } } } BlockUnlinkAllCommand::BlockUnlinkAllCommand(Block *block, QUndoCommand *parent) : UndoCommand(parent), block_(block) { } Project *BlockUnlinkAllCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void BlockUnlinkAllCommand::redo_internal() { unlinked_ = block_->linked_clips(); foreach (Block* link, unlinked_) { Block::Unlink(block_, link); } } void BlockUnlinkAllCommand::undo_internal() { foreach (Block* link, unlinked_) { Block::Link(block_, link); } unlinked_.clear(); } BlockLinkManyCommand::BlockLinkManyCommand(const QList blocks, bool link, QUndoCommand *parent) : UndoCommand(parent), blocks_(blocks) { foreach (Block* a, blocks_) { foreach (Block* b, blocks_) { if (a != b) { new BlockLinkCommand(a, b, link, this); } } } } Project *BlockLinkManyCommand::GetRelevantProject() const { return static_cast(blocks_.first()->parent())->project(); } BlockEnableDisableCommand::BlockEnableDisableCommand(Block *block, bool enabled, QUndoCommand *parent) : UndoCommand(parent), block_(block), old_enabled_(block_->is_enabled()), new_enabled_(enabled) { } Project *BlockEnableDisableCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void BlockEnableDisableCommand::redo_internal() { block_->set_enabled(new_enabled_); } void BlockEnableDisableCommand::undo_internal() { block_->set_enabled(old_enabled_); } BlockTrimCommand::BlockTrimCommand(TrackOutput* track, Block *block, rational new_length, Timeline::MovementMode mode, QUndoCommand *command) : UndoCommand(command), track_(track), block_(block), old_length_(block->length()), new_length_(new_length), mode_(mode), adjacent_(nullptr), we_created_adjacent_(false), we_deleted_adjacent_(false), trim_is_a_roll_edit_(false) { } Project *BlockTrimCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void BlockTrimCommand::redo_internal() { track_->BeginOperation(); // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer rational trim_diff = old_length_ - new_length_; TimeRange invalidate_range; if (mode_ == Timeline::kTrimIn) { invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff); block_->set_length_and_media_in(new_length_); adjacent_ = block_->previous(); } else { invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff); block_->set_length_and_media_out(new_length_); adjacent_ = block_->next(); } if (trim_diff > rational()) { // If trimming SHORTER, we'll need to create/modify a gap if (adjacent_ && (adjacent_->type() == Block::kGap || trim_is_a_roll_edit_)) { // A gap (or equivalent) exists, simply increase the size of it if (mode_ == Timeline::kTrimIn) { adjacent_->set_length_and_media_out(adjacent_->length() + trim_diff); } else { adjacent_->set_length_and_media_in(adjacent_->length() + trim_diff); } } else { // Don't create a gap if the trim was at the end of the sequence (which would be indicated by // the mode being "trim out" and "block_->next()" being null. if (mode_ == Timeline::kTrimIn || block_->next()) { // We must create a gap we_created_adjacent_ = true; adjacent_ = new GapBlock(); adjacent_->set_length_and_media_out(trim_diff); static_cast(track_->parent())->AddNode(adjacent_); if (mode_ == Timeline::kTrimIn) { track_->InsertBlockBefore(adjacent_, block_); } else { track_->InsertBlockAfter(adjacent_, block_); } } } } else { if (adjacent_) { // If trimming LONGER, we'll need to trim the adjacent // (assume if there's no adjacent, we're at the end of the timeline and do nothing) rational adjacent_length = adjacent_->length() + trim_diff; if (adjacent_length.isNull()) { // Ripple remove block track_->RippleRemoveBlock(adjacent_); TakeNodeFromParentGraph(adjacent_, &memory_manager_); we_deleted_adjacent_ = true; } else if (mode_ == Timeline::kTrimIn) { adjacent_->set_length_and_media_out(adjacent_length); } else { adjacent_->set_length_and_media_in(adjacent_length); } } } track_->EndOperation(); if (block_->type() == Block::kTransition) { // Whole transition needs to be invalidated invalidate_range = TimeRange(block_->in(), block_->out()); } track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } void BlockTrimCommand::undo_internal() { TimeRange invalidate_range; if (block_->type() == Block::kTransition) { // Whole transition needs to be invalidated invalidate_range = TimeRange(block_->in(), block_->out()); } track_->BeginOperation(); // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer rational trim_diff = old_length_ - new_length_; if (trim_diff > rational()) { // If trimmed SHORTER, we need to unadjust the gap if (we_created_adjacent_) { // If we created a gap, just remove it straight up track_->RippleRemoveBlock(adjacent_); delete TakeNodeFromParentGraph(adjacent_); adjacent_ = nullptr; we_created_adjacent_ = false; } else if (adjacent_) { // If we adjusted an existing gap, unadjust here adjacent_->set_length_and_media_out(adjacent_->length() - trim_diff); } } else { if (adjacent_) { // If trimmed LONGER, we adjusted an existing block // (assume if there's no adjacent, we're at the end of the timeline and do nothing) if (we_deleted_adjacent_) { static_cast(track_->parent())->AddNode(adjacent_); if (mode_ == Timeline::kTrimIn) { track_->InsertBlockBefore(adjacent_, block_); } else { track_->InsertBlockAfter(adjacent_, block_); } we_deleted_adjacent_ = false; } else if (mode_ == Timeline::kTrimIn) { adjacent_->set_length_and_media_out(adjacent_->length() - trim_diff); } else { adjacent_->set_length_and_media_in(adjacent_->length() - trim_diff); } } } if (mode_ == Timeline::kTrimIn) { block_->set_length_and_media_in(old_length_); if (block_->type() != Block::kTransition) { invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff); } } else { block_->set_length_and_media_out(old_length_); if (block_->type() != Block::kTransition) { invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff); } } track_->EndOperation(); track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } TrackReplaceBlockWithGapCommand::TrackReplaceBlockWithGapCommand(TrackOutput *track, Block *block, QUndoCommand *command) : UndoCommand(command), track_(track), block_(block), existing_gap_(nullptr), existing_merged_gap_(nullptr), our_gap_(nullptr) { } Project *TrackReplaceBlockWithGapCommand::GetRelevantProject() const { return static_cast(block_->parent())->project(); } void TrackReplaceBlockWithGapCommand::redo_internal() { track_->BeginOperation(); // Invalidate the range inhabited by this block TimeRange invalidate_range(block_->in(), block_->out()); if (block_->next()) { // 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 = (previous && previous->type() == Block::kGap); bool next_is_a_gap = (next && next->type() == Block::kGap); 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_); TakeNodeFromParentGraph(existing_merged_gap_, &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 our_gap_ = new GapBlock(); our_gap_->set_length_and_media_out(new_gap_length); static_cast(track_->parent())->AddNode(our_gap_); track_->ReplaceBlock(block_, our_gap_); } } 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 (preceding && preceding->type() == Block::kGap) { track_->RippleRemoveBlock(preceding); TakeNodeFromParentGraph(preceding, &memory_manager_); existing_merged_gap_ = static_cast(preceding); } // Remove block in question track_->RippleRemoveBlock(block_); } track_->EndOperation(); track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } void TrackReplaceBlockWithGapCommand::undo_internal() { track_->BeginOperation(); if (our_gap_) { // We made this gap, simply swap our gap back track_->ReplaceBlock(our_gap_, block_); delete TakeNodeFromParentGraph(our_gap_); our_gap_ = nullptr; } else if (existing_gap_) { // 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(); static_cast(track_->parent())->AddNode(existing_merged_gap_); 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; } 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_) { static_cast(track_->parent())->AddNode(existing_merged_gap_); track_->AppendBlock(existing_merged_gap_); existing_merged_gap_ = nullptr; } // Restore block track_->AppendBlock(block_); } track_->EndOperation(); track_->InvalidateCache(TimeRange(block_->in(), block_->out()), track_->block_input(), track_->block_input()); } TrackSlideCommand::TrackSlideCommand(TrackOutput* track, const QList& moving_blocks, Block *in_adjacent, Block *out_adjacent, const rational& movement, QUndoCommand* parent) : UndoCommand(parent), track_(track), blocks_(moving_blocks), movement_(movement), we_created_in_adjacent_(false), in_adjacent_(in_adjacent), we_created_out_adjacent_(false), out_adjacent_(out_adjacent) { Q_ASSERT(!movement_.isNull()); } Project *TrackSlideCommand::GetRelevantProject() const { return static_cast(track_->parent())->project(); } void TrackSlideCommand::redo_internal() { slide_internal(false); } void TrackSlideCommand::undo_internal() { slide_internal(true); } void TrackSlideCommand::slide_internal(bool undo) { // Make sure all movement blocks' old positions are invalidated TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); track_->BeginOperation(); if (undo) { // Undo code if (we_created_in_adjacent_) { // This is a gap we made, we can just delete it entirely track_->RippleRemoveBlock(in_adjacent_); delete TakeNodeFromParentGraph(in_adjacent_); we_created_in_adjacent_ = false; in_adjacent_ = nullptr; } else if (in_adjacent_->parent() == &memory_manager_) { // This is a gap we removed, we can re-insert it now static_cast(track_->parent())->AddNode(in_adjacent_); track_->InsertBlockBefore(in_adjacent_, blocks_.first()); } else { // We must have just resized this block in_adjacent_->set_length_and_media_out(in_adjacent_->length() - movement_); } if (we_created_out_adjacent_) { // This is a gap we made, we can just delete it entirely track_->RippleRemoveBlock(out_adjacent_); delete TakeNodeFromParentGraph(out_adjacent_); we_created_out_adjacent_ = false; out_adjacent_ = nullptr; } else if (out_adjacent_) { // We may not have created an out adjacent if this was the last clip in the track so we have // to check here if (out_adjacent_->parent() == &memory_manager_) { // This is a gap we removed, we can re-insert it now static_cast(track_->parent())->AddNode(out_adjacent_); track_->InsertBlockAfter(out_adjacent_, blocks_.last()); } else { // We must have just resized this block out_adjacent_->set_length_and_media_in(out_adjacent_->length() + movement_); } } } else { // Redo code if (!in_adjacent_) { // For any slide operation to have occurred at all with no in_adjacent, a gap will need to // be created GapBlock* gap = new GapBlock(); gap->set_length_and_media_out(movement_); static_cast(track_->parent())->AddNode(gap); track_->InsertBlockBefore(gap, blocks_.first()); we_created_in_adjacent_ = true; in_adjacent_ = gap; } else if (-movement_ == in_adjacent_->length()) { // Remove in adjacent entirely track_->RippleRemoveBlock(in_adjacent_); TakeNodeFromParentGraph(in_adjacent_, &memory_manager_); } else { // Resize in adjacent in_adjacent_->set_length_and_media_out(in_adjacent_->length() + movement_); } if (!out_adjacent_) { // For any slide operation to have occurred at all with no out_adjacent, a gap will need to // be created UNLESS this is at the end of the track already if (blocks_.last()->next()) { GapBlock* gap = new GapBlock(); gap->set_length_and_media_out(-movement_); static_cast(track_->parent())->AddNode(gap); track_->InsertBlockAfter(gap, blocks_.last()); we_created_out_adjacent_ = true; out_adjacent_ = gap; } } else if (movement_ == out_adjacent_->length()) { // Remove out adjacent entirely track_->RippleRemoveBlock(out_adjacent_); TakeNodeFromParentGraph(out_adjacent_, &memory_manager_); } else { // Resize out adjacent out_adjacent_->set_length_and_media_in(out_adjacent_->length() - movement_); } } track_->EndOperation(); // Make sure all movement blocks' new positions are invalidated invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), qMax(invalidate_range.out(), blocks_.last()->out())); track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } 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 if (list_->type() == Timeline::kTrackTypeVideo) { static_cast(list_->parent())->ShiftVideoCache(out_, in_); } else if (list_->type() == Timeline::kTrackTypeAudio) { static_cast(list_->parent())->ShiftAudioCache(out_, in_); } foreach (TrackOutput* track, working_tracks_) { track->BeginOperation(); } } foreach (TrackRippleRemoveAreaCommand* c, commands_) { c->redo(); } if (all_tracks_unlocked_) { foreach (TrackOutput* track, working_tracks_) { track->EndOperation(); } } } 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 if (list_->type() == Timeline::kTrackTypeVideo) { static_cast(list_->parent())->ShiftVideoCache(in_, out_); } else if (list_->type() == Timeline::kTrackTypeAudio) { static_cast(list_->parent())->ShiftAudioCache(in_, out_); } foreach (TrackOutput* track, working_tracks_) { track->BeginOperation(); } } foreach (TrackRippleRemoveAreaCommand* c, commands_) { c->undo(); } if (all_tracks_unlocked_) { foreach (TrackOutput* track, working_tracks_) { track->EndOperation(); } } } 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->BeginOperation(); } 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;i 0) { new_latest_pt = qMax(new_latest_pt, info.block->out()); } else { new_latest_pt = qMax(new_latest_pt, info.block->in()); } } else if (info.new_length > 0) { 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())->ShiftVideoCache(old_latest_pt, new_latest_pt); } else if (track_list_->type() == Timeline::kTrackTypeAudio) { static_cast(track_list_->parent())->ShiftAudioCache(old_latest_pt, new_latest_pt); } foreach (const RippleInfo& info, info_) { info.track->EndOperation(); // 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() { // FIXME: Add cache shift optimization // 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); } } } TrackListInsertGaps::TrackListInsertGaps(TrackList *track_list, const rational &point, const rational &length, QUndoCommand *parent) : UndoCommand(parent), track_list_(track_list), point_(point), length_(length), split_command_(nullptr) { all_tracks_unlocked_ = true; foreach (TrackOutput* track, track_list_->GetTracks()) { if (track->IsLocked()) { all_tracks_unlocked_ = false; continue; } working_tracks_.append(track); } } Project *TrackListInsertGaps::GetRelevantProject() const { return static_cast(static_cast(track_list_->parent())->parent())->project(); } void TrackListInsertGaps::redo_internal() { if (all_tracks_unlocked_) { // Optimize by shifting over since we have a constant amount of time being inserted if (track_list_->type() == Timeline::kTrackTypeVideo) { static_cast(track_list_->parent())->ShiftVideoCache(point_, point_ + length_); } else if (track_list_->type() == Timeline::kTrackTypeAudio) { static_cast(track_list_->parent())->ShiftAudioCache(point_, point_ + length_); } foreach (TrackOutput* track, working_tracks_) { track->BeginOperation(); } } QVector blocks_to_split; QList blocks_to_append_gap_to; foreach (TrackOutput* track, working_tracks_) { foreach (Block* b, track->Blocks()) { if (b->type() == Block::kGap && b->in() <= point_ && b->out() >= point_) { gaps_to_extend_.append(b); } else if (b->type() == Block::kClip && b->out() >= point_) { if (b->out() > point_) { blocks_to_split.append(b); } blocks_to_append_gap_to.append(b); } } } foreach (Block* gap, gaps_to_extend_) { gap->set_length_and_media_out(gap->length() + length_); } if (!blocks_to_split.isEmpty()) { split_command_ = new BlockSplitPreservingLinksCommand(blocks_to_split, {point_}); split_command_->redo(); } foreach (Block* block, blocks_to_append_gap_to) { GapBlock* gap = new GapBlock(); gap->set_length_and_media_out(length_); static_cast(block->parent())->AddNode(gap); TrackOutput::TrackFromBlock(block)->InsertBlockAfter(gap, block); gaps_added_.append(gap); } if (all_tracks_unlocked_) { foreach (TrackOutput* track, working_tracks_) { track->EndOperation(); } } } void TrackListInsertGaps::undo_internal() { if (all_tracks_unlocked_) { // Optimize by shifting over since we have a constant amount of time being inserted if (track_list_->type() == Timeline::kTrackTypeVideo) { static_cast(track_list_->parent())->ShiftVideoCache(point_ + length_, point_); } else if (track_list_->type() == Timeline::kTrackTypeAudio) { static_cast(track_list_->parent())->ShiftAudioCache(point_ + length_, point_); } foreach (TrackOutput* track, working_tracks_) { track->BeginOperation(); } } // Remove added gaps foreach (GapBlock* gap, gaps_added_) { TrackOutput::TrackFromBlock(gap)->RippleRemoveBlock(gap); delete TakeNodeFromParentGraph(gap); } gaps_added_.clear(); // Un-split blocks if (split_command_) { split_command_->undo(); delete split_command_; split_command_ = nullptr; } // Restore original length of gaps foreach (Block* gap, gaps_to_extend_) { gap->set_length_and_media_out(gap->length() - length_); } gaps_to_extend_.clear(); if (all_tracks_unlocked_) { foreach (TrackOutput* track, working_tracks_) { track->EndOperation(); } } } TransitionRemoveCommand::TransitionRemoveCommand(TrackOutput* track, TransitionBlock *block, QUndoCommand* parent) : UndoCommand(parent), track_(track), block_(block), out_block_(block_->connected_out_block()), in_block_(block_->connected_in_block()) { // Can't remove a transition in this way unless it's connected to at least one other block Q_ASSERT(out_block_ || in_block_); } Project *TransitionRemoveCommand::GetRelevantProject() const { return static_cast(track_->parent())->project(); } void TransitionRemoveCommand::redo_internal() { 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_) { NodeParam::DisconnectEdge(in_block_->output(), block_->in_block_input()); } if (out_block_) { NodeParam::DisconnectEdge(out_block_->output(), block_->out_block_input()); } track_->RippleRemoveBlock(block_); track_->EndOperation(); track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } void TransitionRemoveCommand::undo_internal() { track_->BeginOperation(); if (in_block_) { track_->InsertBlockBefore(block_, in_block_); } else { track_->InsertBlockAfter(block_, out_block_); } if (in_block_) { NodeParam::ConnectEdge(in_block_->output(), block_->in_block_input()); } if (out_block_) { NodeParam::ConnectEdge(out_block_->output(), block_->out_block_input()); } // 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_->InvalidateCache(TimeRange(block_->in(), block_->out()), track_->block_input(), track_->block_input()); } OLIVE_NAMESPACE_EXIT