/*** Olive - Non-Linear Video Editor Copyright (C) 2021 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 . ***/ #ifndef TIMELINEUNDOABLE_H #define TIMELINEUNDOABLE_H #include "config/config.h" #include "core.h" #include "node/block/block.h" #include "node/block/clip/clip.h" #include "node/block/gap/gap.h" #include "node/block/transition/transition.h" #include "node/math/math/math.h" #include "node/math/merge/merge.h" #include "node/graph.h" #include "node/output/track/track.h" #include "node/output/track/tracklist.h" #include "timeline/timelinepoints.h" #include "undo/undocommand.h" #include "widget/nodeview/nodeviewundo.h" namespace olive { inline bool NodeCanBeRemoved(Node* n) { return n->output_connections().empty(); } inline UndoCommand* CreateRemoveCommand(Node* n) { return new NodeRemoveWithExclusiveDependenciesAndDisconnect(n); } inline UndoCommand* CreateAndRunRemoveCommand(Node* n) { UndoCommand* command = CreateRemoveCommand(n); command->redo(); return command; } class BlockResizeCommand : public UndoCommand { public: BlockResizeCommand(Block* block, rational new_length) : block_(block), new_length_(new_length) { } virtual Project* GetRelevantProject() const override { return block_->project(); } virtual void redo() override { old_length_ = block_->length(); block_->set_length_and_media_out(new_length_); } virtual void undo() override { block_->set_length_and_media_out(old_length_); } private: Block* block_; rational old_length_; rational new_length_; }; class BlockResizeWithMediaInCommand : public UndoCommand { public: BlockResizeWithMediaInCommand(Block* block, rational new_length) : block_(block), new_length_(new_length) { } virtual Project* GetRelevantProject() const override { return block_->project(); } virtual void redo() override { old_length_ = block_->length(); block_->set_length_and_media_in(new_length_); } virtual void undo() override { block_->set_length_and_media_in(old_length_); } private: Block* block_; rational old_length_; rational new_length_; }; /** * @brief Performs a trim in the timeline that only affects the block and the block adjacent * * Changes the length of one block while also changing the length of the block directly adjacent * to compensate so that the rest of the track is unaffected. * * By default, this will only affect the length of gaps. If the adjacent needs to increase its * length and is not a gap, a gap will be created and inserted to fill that time. This command can * be set to always trim even if the adjacent clip isn't a gap with SetTrimIsARollEdit() */ class BlockTrimCommand : public UndoCommand { public: BlockTrimCommand(Track *track, Block* block, rational new_length, Timeline::MovementMode mode); virtual ~BlockTrimCommand() override { delete deleted_adjacent_command_; } virtual Project* GetRelevantProject() const override { return track_->project(); } /** * @brief Set this if the trim should always affect the adjacent clip and not create a gap */ void SetTrimIsARollEdit(bool e) { trim_is_a_roll_edit_ = e; } /** * @brief Set whether adjacent blocks set to zero length should be removed from the whole graph * * If an adjacent block's length is set to 0, it's automatically removed from the track. By * default it also gets removed from the whole graph. Set this to FALSE to disable that * functionality. */ void SetRemoveZeroLengthFromGraph(bool e) { remove_block_from_graph_ = e; } virtual void redo() override; virtual void undo() override; private: void prep(); bool prepped_; bool doing_nothing_; rational trim_diff_; Track* track_; Block* block_; rational old_length_; rational new_length_; Timeline::MovementMode mode_; Block* adjacent_; bool needs_adjacent_; bool we_created_adjacent_; bool we_removed_adjacent_; UndoCommand* deleted_adjacent_command_; bool trim_is_a_roll_edit_; bool remove_block_from_graph_; QObject memory_manager_; }; class BlockSetMediaInCommand : public UndoCommand { public: BlockSetMediaInCommand(Block* block, rational new_media_in) : block_(block), new_media_in_(new_media_in) { } virtual Project* GetRelevantProject() const override { return block_->project(); } virtual void redo() override { old_media_in_ = block_->media_in(); block_->set_media_in(new_media_in_); } virtual void undo() override { block_->set_media_in(old_media_in_); } private: Block* block_; rational old_media_in_; rational new_media_in_; }; class TrackRippleRemoveBlockCommand : public UndoCommand { public: TrackRippleRemoveBlockCommand(Track* track, Block* block) : track_(track), block_(block) { } virtual Project* GetRelevantProject() const override { return track_->project(); } virtual void redo() override { before_ = block_->previous(); track_->RippleRemoveBlock(block_); } virtual void undo() override { track_->InsertBlockAfter(block_, before_); } private: Track* track_; Block* block_; Block* before_; }; class TrackPrependBlockCommand : public UndoCommand { public: TrackPrependBlockCommand(Track* track, Block* block) : track_(track), block_(block) { } virtual Project* GetRelevantProject() const override { return track_->project(); } virtual void redo() override { track_->PrependBlock(block_); } virtual void undo() override { track_->RippleRemoveBlock(block_); } private: Track* track_; Block* block_; }; class TrackInsertBlockAfterCommand : public UndoCommand { public: TrackInsertBlockAfterCommand(Track* track, Block* block, Block* before) : track_(track), block_(block), before_(before) { } virtual Project* GetRelevantProject() const override { return block_->project(); } virtual void redo() override { track_->InsertBlockAfter(block_, before_); } virtual void undo() override { track_->RippleRemoveBlock(block_); } private: Track* track_; Block* block_; Block* before_; }; class BlockSplitCommand : public UndoCommand { public: BlockSplitCommand(Block* block, rational point) : block_(block), new_block_(nullptr), point_(point), reconnect_tree_command_(nullptr), position_command_(nullptr) { } virtual ~BlockSplitCommand() override { delete reconnect_tree_command_; delete position_command_; } virtual Project* GetRelevantProject() const override { return block_->project(); } /** * @brief Access the second block created as a result. Only valid after redo(). */ Block* new_block() { return new_block_; } virtual void redo() override { old_length_ = block_->length(); Q_ASSERT(point_ > block_->in() && point_ < block_->out()); if (!reconnect_tree_command_) { reconnect_tree_command_ = new MultiUndoCommand(); new_block_ = static_cast(Node::CopyNodeInGraph(block_, reconnect_tree_command_)); } reconnect_tree_command_->redo(); // Determine our new lengths rational new_length = point_ - block_->in(); rational new_part_length = block_->out() - point_; // Begin an operation Track* track = block_->track(); track->BeginOperation(); // Set lengths block_->set_length_and_media_out(new_length); new_block()->set_length_and_media_in(new_part_length); // Insert new block track->InsertBlockAfter(new_block(), block_); // Position the block if (!position_command_) { position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, new_block()->index(), track->Blocks().size(), true); } position_command_->redo(); // If the block had an out transition, we move it to the new block moved_transition_ = NodeInput(); TransitionBlock* potential_transition = dynamic_cast(new_block()->next()); if (potential_transition) { for (const Node::OutputConnection& output : block_->output_connections()) { if (output.second.node() == potential_transition) { moved_transition_ = NodeInput(potential_transition, TransitionBlock::kOutBlockInput); Node::DisconnectEdge(block_, moved_transition_); Node::ConnectEdge(new_block(), moved_transition_); break; } } } track->EndOperation(); } virtual void undo() override { Track* track = block_->track(); track->BeginOperation(); if (moved_transition_.IsValid()) { Node::DisconnectEdge(new_block(), moved_transition_); Node::ConnectEdge(block_, moved_transition_); } position_command_->undo(); block_->set_length_and_media_out(old_length_); track->RippleRemoveBlock(new_block()); // If we ran a reconnect command, disconnect now reconnect_tree_command_->undo(); track->EndOperation(); } private: Block* block_; Block* new_block_; rational old_length_; rational point_; MultiUndoCommand* reconnect_tree_command_; NodeInput moved_transition_; NodeSetPositionAsChildCommand* position_command_; }; class BlockSplitPreservingLinksCommand : public UndoCommand { public: BlockSplitPreservingLinksCommand(const QVector &blocks, const QList& times) : blocks_(blocks), times_(times) { } virtual ~BlockSplitPreservingLinksCommand() override { qDeleteAll(commands_); } virtual Project* GetRelevantProject() const override { return blocks_.first()->project(); } virtual void redo() override { if (commands_.isEmpty()) { QVector< QVector > split_blocks(times_.size()); for (int i=0;i times_.at(i-1)); QVector splits(blocks_.size()); for (int j=0;jin() < time && b->out() > time) { BlockSplitCommand* split_command = new BlockSplitCommand(b, time); split_command->redo(); splits.replace(j, split_command->new_block()); commands_.append(split_command); } 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) { NodeLinkCommand* blc = new NodeLinkCommand(split_list.at(i), split_list.at(j), true); blc->redo(); commands_.append(blc); } } } } } else { for (int i=0; iredo(); } } } virtual void undo() override { for (int i=commands_.size()-1; i>=0; i--) { commands_.at(i)->undo(); } } private: QVector blocks_; QList times_; QVector commands_; }; class TrackSplitAtTimeCommand : public UndoCommand { public: TrackSplitAtTimeCommand(Track* track, rational point) : prepped_(false), track_(track), point_(point), command_(nullptr) { } virtual ~TrackSplitAtTimeCommand() override { delete command_; } virtual Project* GetRelevantProject() const override { return track_->project(); } virtual void redo() override { if (!prepped_) { // Find Block that contains this time Block* b = track_->BlockContainingTime(point_); if (b) { command_ = new BlockSplitCommand(b, point_); } prepped_ = true; } if (command_) { command_->redo(); } } virtual void undo() override { if (command_) { command_->undo(); } } private: bool prepped_; Track* track_; rational point_; UndoCommand* command_; }; /** * @brief Clears the area between in and out * * The area between `in` and `out` is guaranteed to be freed. BLocks are trimmed and removed to free this space. * By default, nothing takes this area meaning all subsequent clips are pushed backward, however you can specify * a block to insert at the `in` point. No checking is done to ensure `insert` is the same length as `in` to `out`. */ class TrackRippleRemoveAreaCommand : public UndoCommand { public: TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range) : prepped_(false), track_(track), range_(range), splice_split_command_(nullptr) { trim_out_.block = nullptr; trim_in_.block = nullptr; } virtual ~TrackRippleRemoveAreaCommand() override { delete splice_split_command_; qDeleteAll(remove_block_commands_); } virtual Project* GetRelevantProject() const override { return track_->project(); } /** * @brief Block to insert after if you want to insert something between this ripple */ Block* GetInsertionIndex() const { return insert_previous_; } Block* GetSplicedBlock() const { if (splice_split_command_) { return splice_split_command_->new_block(); } return nullptr; } virtual void redo() override { if (!prepped_) { prep(); prepped_ = true; } track_->BeginOperation(); if (splice_split_command_) { // We're just splicing splice_split_command_->redo(); // Trim the in of the split Block* split = splice_split_command_->new_block(); split->set_length_and_media_in(split->length() - (range_.out() - split->in())); } else { if (trim_out_.block) { trim_out_.block->set_length_and_media_out(trim_out_.new_length); } if (trim_in_.block) { trim_in_.block->set_length_and_media_in(trim_in_.new_length); } // Perform removals if (!removals_.isEmpty()) { foreach (auto op, removals_) { // Ripple remove them all first track_->RippleRemoveBlock(op.block); } // Create undo commands for node removals where possible if (remove_block_commands_.isEmpty()) { foreach (auto op, removals_) { if (NodeCanBeRemoved(op.block)) { remove_block_commands_.append(CreateRemoveCommand(op.block)); } } } foreach (UndoCommand* c, remove_block_commands_) { c->redo(); } } } track_->EndOperation(); track_->Node::InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX), Track::kBlockInput); } virtual void undo() override { // Begin operations track_->BeginOperation(); if (splice_split_command_) { splice_split_command_->undo(); } else { if (trim_out_.block) { trim_out_.block->set_length_and_media_out(trim_out_.old_length); } if (trim_in_.block) { trim_in_.block->set_length_and_media_in(trim_in_.old_length); } // Un-remove any blocks for (int i=remove_block_commands_.size()-1; i>=0; i--) { remove_block_commands_.at(i)->undo(); } foreach (auto op, removals_) { track_->InsertBlockAfter(op.block, op.before); } } // End operations and invalidate track_->EndOperation(); track_->Node::InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX), Track::kBlockInput); } private: bool prepped_; void prep() { // Determine precisely what will be happening to these tracks Block* first_block = track_->NearestBlockBeforeOrAt(range_.in()); if (!first_block) { // No blocks at this time, nothing to be done on this track return; } // Determine if this first block is getting trimmed or removed bool first_block_is_out_trimmed = first_block->in() < range_.in(); bool first_block_is_in_trimmed = first_block->out() > range_.out(); // Set's the block that any insert command should insert AFTER. If the first block is not // getting out-trimmed, that means first block is either getting removed or in-trimmed, which // means any insert should happen before it insert_previous_ = first_block_is_out_trimmed ? first_block : first_block->previous(); // If it's getting trimmed, determine if it's actually getting spliced if (first_block_is_out_trimmed && first_block_is_in_trimmed) { // This block is getting spliced, so we'll handle that later splice_split_command_ = new BlockSplitCommand(first_block, range_.in()); } else { // It's just getting trimmed or removed, so we'll append that operation if (first_block_is_out_trimmed) { trim_out_ = {first_block, first_block->length(), first_block->length() - (first_block->out() - range_.in())}; } else if (first_block_is_in_trimmed) { // Block is getting in trimmed trim_in_ = {first_block, first_block->length(), first_block->length() - (range_.out() - first_block->in())}; } else { // We know for sure this block is within the range so it will be removed removals_.append(RemoveOperation({first_block, first_block->previous()})); } // If the first block is getting in trimmed, we're already at the end of our range if (!first_block_is_in_trimmed) { // Loop through the rest of the blocks and determine what to do with those for (Block* next=first_block->next(); next; next=next->next()) { bool trimming = (next->out() > range_.out()); if (trimming) { trim_in_ = {next, next->length(), next->length() - (range_.out() - next->in())}; break; } else { removals_.append(RemoveOperation({next, next->previous()})); if (next->out() == range_.out()) { break; } } } } } } struct TrimOperation { Block* block; rational old_length; rational new_length; }; struct RemoveOperation { Block* block; Block* before; }; Track* track_; TimeRange range_; TrimOperation trim_out_; QVector removals_; TrimOperation trim_in_; Block* insert_previous_; BlockSplitCommand* splice_split_command_; QVector remove_block_commands_; }; class TrackListRippleRemoveAreaCommand : public UndoCommand { public: TrackListRippleRemoveAreaCommand(TrackList* list, rational in, rational out) : list_(list), in_(in), out_(out) { } virtual ~TrackListRippleRemoveAreaCommand() override { qDeleteAll(commands_); } virtual Project* GetRelevantProject() const override { return list_->parent()->project(); } virtual void redo() override { // Code that's only run on the first redo if (commands_.isEmpty()) { all_tracks_unlocked_ = true; foreach (Track* track, list_->GetTracks()) { if (track->IsLocked()) { all_tracks_unlocked_ = false; continue; } TrackRippleRemoveAreaCommand* c = new TrackRippleRemoveAreaCommand(track, TimeRange(in_, out_)); commands_.append(c); working_tracks_.append(track); } } 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() == Track::kVideo) { list_->parent()->ShiftVideoCache(out_, in_); } else if (list_->type() == Track::kAudio) { list_->parent()->ShiftAudioCache(out_, in_); } foreach (Track* track, working_tracks_) { track->BeginOperation(); } } foreach (TrackRippleRemoveAreaCommand* c, commands_) { c->redo(); } if (all_tracks_unlocked_) { foreach (Track* track, working_tracks_) { track->EndOperation(); } } } virtual void undo() override { 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() == Track::kVideo) { list_->parent()->ShiftVideoCache(in_, out_); } else if (list_->type() == Track::kAudio) { list_->parent()->ShiftAudioCache(in_, out_); } foreach (Track* track, working_tracks_) { track->BeginOperation(); } } foreach (TrackRippleRemoveAreaCommand* c, commands_) { c->undo(); } if (all_tracks_unlocked_) { foreach (Track* track, working_tracks_) { track->EndOperation(); } } } private: TrackList* list_; QList working_tracks_; rational in_; rational out_; bool all_tracks_unlocked_; QVector commands_; }; class TimelineRippleRemoveAreaCommand : public MultiUndoCommand { public: TimelineRippleRemoveAreaCommand(Sequence* timeline, rational in, rational out) : timeline_(timeline) { for (int i=0; itrack_list(static_cast(i)), in, out)); } } virtual Project* GetRelevantProject() const override { return timeline_->project(); } private: Sequence* timeline_; }; class TrackListRippleToolCommand : public UndoCommand { public: struct RippleInfo { Block* block; bool append_gap; }; TrackListRippleToolCommand(TrackList* track_list, const QHash& info, const rational& ripple_movement, const Timeline::MovementMode& movement_mode) : track_list_(track_list), info_(info), ripple_movement_(ripple_movement), movement_mode_(movement_mode) { all_tracks_unlocked_ = (info_.size() == track_list_->GetTrackCount()); } virtual Project* GetRelevantProject() const override { return track_list_->parent()->project(); } virtual void redo() override { ripple(true); } virtual void undo() override { ripple(false); } private: void ripple(bool redo) { if (info_.isEmpty()) { return; } // The following variables are used to determine how much of the cache to invalidate // If we can shift, we will shift from the latest out before the ripple to the latest out after, // since those sections will be unchanged by this ripple rational pre_latest_out = RATIONAL_MIN; rational post_latest_out = RATIONAL_MIN; // Make timeline changes for (auto it=info_.cbegin(); it!=info_.cend(); it++) { Track* track = it.key(); const RippleInfo& info = it.value(); WorkingData working_data = working_data_.value(track); Block* b = info.block; // Generate block length rational new_block_length; rational operation_movement = ripple_movement_; if (movement_mode_ == Timeline::kTrimIn) { operation_movement = -operation_movement; } if (!redo) { operation_movement = -operation_movement; } if (b) { new_block_length = b->length() + operation_movement; } rational pre_shift; rational post_shift; // Begin operation so we can invalidate better later track->BeginOperation(); if (info.append_gap) { // Rather than rippling the referenced block, we'll insert a gap and ripple with that GapBlock* gap = working_data.created_gap; if (redo) { if (!gap) { gap = new GapBlock(); gap->set_length_and_media_out(qAbs(ripple_movement_)); working_data.created_gap = gap; } gap->setParent(track->parent()); track->InsertBlockBefore(gap, b); // As an insertion, we will shift from the gap's in to the gap's out pre_shift = gap->in(); post_shift = gap->out(); working_data.earliest_point_of_change = gap->in(); } else { // As a removal, we will shift from the gap's out to the gap's in pre_shift = gap->out(); post_shift = gap->in(); track->RippleRemoveBlock(gap); gap->setParent(&memory_manager_); } } else if ((redo && new_block_length.isNull()) || (!redo && !b->track())) { // The ripple is the length of this block. We assume that for this to happen, it must have // been a gap that we will now remove. if (redo) { // The earliest point changes will happen is at the start of this block working_data.earliest_point_of_change = b->in(); // As a removal, we will be shifting from the out point to the in point pre_shift = b->out(); post_shift = b->in(); // Remove gap from track and from graph working_data.removed_gap_after = b->previous(); track->RippleRemoveBlock(b); b->setParent(&memory_manager_); } else { // Restore gap to graph and track b->setParent(track->parent()); track->InsertBlockAfter(b, working_data.removed_gap_after); // The earliest point changes will happen is at the start of this block working_data.earliest_point_of_change = b->in(); // As an insert, we will be shifting from the block's in point to its out point pre_shift = b->in(); post_shift = b->out(); } } else { // Store old length working_data.old_length = b->length(); if (movement_mode_ == Timeline::kTrimIn) { // The earliest point changes will occur is in point of this bloc working_data.earliest_point_of_change = b->in(); // Undo the trim in inversion we do above, this will still be inverted accurately for // undoing where appropriate rational inverted = -operation_movement; if (inverted > 0) { pre_shift = b->in() + inverted; post_shift = b->in(); } else { pre_shift = b->in(); post_shift = b->in() - inverted; } // Update length b->set_length_and_media_in(new_block_length); } else { // The earliest point changes will occur is the out point if trimming out or the in point // if trimming in working_data.earliest_point_of_change = b->out(); // The latest out before the ripple is this block's current out point pre_shift = b->out(); // Update length b->set_length_and_media_out(new_block_length); // The latest out after the ripple is this block's out point after the length change post_shift = b->out(); } } working_data_.insert(it.key(), working_data); pre_latest_out = qMax(pre_latest_out, pre_shift); post_latest_out = qMax(post_latest_out, post_shift); } if (all_tracks_unlocked_) { // We rippled all the tracks, so we can shift the whole cache if (track_list_->type() == Track::kVideo) { track_list_->parent()->ShiftVideoCache(pre_latest_out, post_latest_out); } else if (track_list_->type() == Track::kAudio) { track_list_->parent()->ShiftAudioCache(pre_latest_out, post_latest_out); } } for (auto it=working_data_.cbegin(); it!=working_data_.cend(); it++) { Track* track = it.key(); track->EndOperation(); if (!all_tracks_unlocked_) { // If we're not shifting, the whole track must get invalidated track->Node::InvalidateCache(TimeRange(it.value().earliest_point_of_change, RATIONAL_MAX), Track::kBlockInput); } } } TrackList* track_list_; QHash info_; rational ripple_movement_; Timeline::MovementMode movement_mode_; struct WorkingData { GapBlock* created_gap = nullptr; Block* removed_gap_after; rational old_length; rational earliest_point_of_change; }; QHash working_data_; QObject memory_manager_; bool all_tracks_unlocked_; }; class TimelineAddTrackCommand : public UndoCommand { public: TimelineAddTrackCommand(TrackList *timeline) { Init(timeline, Config::Current()[QStringLiteral("AutoMergeTracks")].toBool()); } TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks) { Init(timeline, automerge_tracks); } static Track* RunImmediately(TrackList *timeline) { TimelineAddTrackCommand c(timeline); c.redo(); return c.track(); } static Track* RunImmediately(TrackList *timeline, bool automerge) { TimelineAddTrackCommand c(timeline, automerge); c.redo(); return c.track(); } virtual ~TimelineAddTrackCommand() override { delete position_command_; } Track* track() const { return track_; } virtual Project* GetRelevantProject() const override { return timeline_->parent()->project(); } virtual void redo() override { // Add track track_->setParent(timeline_->GetParentGraph()); timeline_->ArrayAppend(); int track_total_index = timeline_->parent()->GetTracks().size(); if (!position_command_) { position_command_ = new NodeSetPositionAsChildCommand(track_, timeline_->parent(), track_total_index, track_total_index + 1, true); } position_command_->redo(); Node::ConnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); // Add merge if applicable if (merge_) { merge_->setParent(timeline_->GetParentGraph()); Track* last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); // Whatever this track used to be connected to, connect the merge instead const Node::OutputConnections edges = last_track->output_connections(); for (const Node::OutputConnection& ic : edges) { const NodeInput& i = ic.second; // Ignore the track input, but funnel everything else through our merge if (i.node() != timeline_->parent() || i.input() != timeline_->track_input()) { Node::DisconnectEdge(last_track, i); Node::ConnectEdge(merge_, i); } } // Connect this as the "blend" track Node::ConnectEdge(track_, blend_); Node::ConnectEdge(last_track, base_); } else if (timeline_->GetTrackCount() == 1) { // If this was the first track we added, QString relevant_input; if (timeline_->type() == Track::kVideo) { relevant_input = ViewerOutput::kTextureInput; } else if (timeline_->type() == Track::kAudio) { relevant_input = ViewerOutput::kSamplesInput; } if (!relevant_input.isEmpty() && !timeline_->parent()->IsInputConnected(relevant_input)) { direct_ = NodeInput(timeline_->parent(), relevant_input); Node::ConnectEdge(track_, direct_); } else { direct_ = NodeInput(); } } } virtual void undo() override { // Remove merge if applicable if (merge_) { // Assume whatever this merge is connected to USED to be connected to the last track Track* last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); Node::DisconnectEdge(track_, blend_); Node::DisconnectEdge(last_track, base_); // Make copy of edges since the node's internal array will change as we disconnect things const Node::OutputConnections edges = merge_->output_connections(); for (const Node::OutputConnection& ic : edges) { const NodeInput& i = ic.second; Node::DisconnectEdge(merge_, i); Node::ConnectEdge(last_track, i); } merge_->setParent(&memory_manager_); } else if (direct_.IsValid()) { Node::DisconnectEdge(track_, direct_); } // Remove track Node::DisconnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); position_command_->undo(); timeline_->ArrayRemoveLast(); track_->setParent(&memory_manager_); } private: void Init(TrackList* timeline, bool automerge) { timeline_ = timeline; position_command_ = nullptr; track_ = new Track(); track_->setParent(&memory_manager_); if (timeline->GetTrackCount() > 0 && automerge) { if (timeline_->type() == Track::kVideo) { merge_ = new MergeNode(); base_ = NodeInput(merge_, MergeNode::kBaseIn); blend_ = NodeInput(merge_, MergeNode::kBlendIn); } else if (timeline_->type() == Track::kAudio) { merge_ = new MathNode(); base_ = NodeInput(merge_, MathNode::kParamAIn); blend_ = NodeInput(merge_, MathNode::kParamBIn); } else { merge_ = nullptr; } } else { merge_ = nullptr; } if (merge_) { merge_->setParent(&memory_manager_); } } TrackList* timeline_; Track* track_; Node* merge_; NodeInput base_; NodeInput blend_; NodeInput direct_; NodeSetPositionAsChildCommand* position_command_; QObject memory_manager_; }; /** * @brief Destructively places `block` at the in point `start` * * The Block is guaranteed to be placed at the starting point specified. If there are Blocks in this area, they are * either trimmed or removed to make space for this Block. Additionally, if the Block is placed beyond the end of * the Sequence, a GapBlock is inserted to compensate. */ class TrackPlaceBlockCommand : public UndoCommand { public: TrackPlaceBlockCommand(TrackList *timeline, int track, Block* block, rational in) : timeline_(timeline), track_index_(track), in_(in), gap_(nullptr), insert_(block), ripple_remove_command_(nullptr) { } virtual ~TrackPlaceBlockCommand() override { delete ripple_remove_command_; qDeleteAll(add_track_commands_); qDeleteAll(position_commands_); } virtual Project* GetRelevantProject() const override { return timeline_->parent()->project(); } virtual void redo() override { // Determine if we need to add tracks if (track_index_ >= timeline_->GetTracks().size()) { if (add_track_commands_.isEmpty()) { // First redo, create tracks now add_track_commands_.resize(track_index_ - timeline_->GetTracks().size() + 1); for (int i=0; iredo(); } } Track* track = timeline_->GetTrackAt(track_index_); track->BeginOperation(); bool 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 if (!gap_) { gap_ = new GapBlock(); gap_->set_length_and_media_out(in_ - track->track_length()); } gap_->setParent(track->parent()); track->AppendBlock(gap_); } track->AppendBlock(insert_); if (position_commands_.isEmpty()) { // Create position commands for insert and gap if necessary if (gap_) { position_commands_.append(new NodeSetPositionAsChildCommand(gap_, track, gap_->index(), track->Blocks().size(), true)); } position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, insert_->index(), track->Blocks().size(), true)); } } else { // Place the Block at this point if (!ripple_remove_command_) { ripple_remove_command_ = new TrackRippleRemoveAreaCommand(track, TimeRange(in_, in_ + insert_->length())); } ripple_remove_command_->redo(); track->InsertBlockAfter(insert_, ripple_remove_command_->GetInsertionIndex()); if (position_commands_.isEmpty()) { position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, insert_->index(), track->Blocks().size(), true)); } } track->EndOperation(); if (ripple_remove_command_) { track->Node::InvalidateCache(TimeRange(insert_->in(), insert_->out()), Track::kBlockInput); } for (int i=0; iredo(); } } virtual void undo() override { for (int i=position_commands_.size()-1; i>=0; i--) { position_commands_.at(i)->undo(); } Track* t = timeline_->GetTrackAt(track_index_); TimeRange insert_range(insert_->in(), insert_->out()); // Firstly, remove our insert t->BeginOperation(); t->RippleRemoveBlock(insert_); if (ripple_remove_command_) { // If we ripple removed, just undo that ripple_remove_command_->undo(); } else if (gap_) { t->RippleRemoveBlock(gap_); gap_->setParent(&memory_manager_); } t->EndOperation(); if (ripple_remove_command_) { t->Node::InvalidateCache(insert_range, Track::kBlockInput); } // Remove tracks if we added them for (int i=add_track_commands_.size()-1; i>=0; i--) { add_track_commands_.at(i)->undo(); } } private: TrackList* timeline_; int track_index_; rational in_; GapBlock* gap_; Block* insert_; QVector add_track_commands_; QObject memory_manager_; TrackRippleRemoveAreaCommand* ripple_remove_command_; QVector position_commands_; }; /** * @brief Replaces Block `old` with Block `replace` * * Both blocks must have equal lengths. */ class TrackReplaceBlockCommand : public UndoCommand { public: TrackReplaceBlockCommand(Track* track, Block* old, Block* replace) : track_(track), old_(old), replace_(replace) { } virtual Project* GetRelevantProject() const override { return track_->project(); } virtual void redo() override { track_->ReplaceBlock(old_, replace_); } virtual void undo() override { track_->ReplaceBlock(replace_, old_); } private: Track* track_; Block* old_; Block* replace_; }; 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) : track_(track), block_(block), existing_gap_(nullptr), existing_merged_gap_(nullptr), our_gap_(nullptr), position_command_(nullptr) { } virtual ~TrackReplaceBlockWithGapCommand() override { delete position_command_; } virtual Project* GetRelevantProject() const override { return block_->project(); } virtual void redo() override; virtual void undo() override; private: void CreateRemoveTransitionCommandIfNecessary(bool next); Track* track_; Block* block_; GapBlock* existing_gap_; GapBlock* existing_merged_gap_; bool existing_gap_precedes_; GapBlock* our_gap_; NodeSetPositionAsChildCommand* position_command_; QObject memory_manager_; QVector transition_remove_commands_; }; class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { public: TimelineRippleDeleteGapsAtRegionsCommand(Sequence* vo, const TimeRangeList& regions) : timeline_(vo), regions_(regions) { } virtual ~TimelineRippleDeleteGapsAtRegionsCommand() override { qDeleteAll(commands_); } virtual Project* GetRelevantProject() const override { return timeline_->project(); } virtual void redo() override { if (commands_.isEmpty()) { foreach (const TimeRange& range, regions_) { rational max_ripple_length = range.length(); QVector blocks_around_range; foreach (Track* 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 (dynamic_cast(block_at_time)) { 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 commands_.append(new TrackRippleRemoveBlockCommand(resize->track(), resize)); } else { // Resize block commands_.append(new BlockResizeCommand(resize, resize->length() - max_ripple_length)); } } } } } foreach (UndoCommand* c, commands_) { c->redo(); } } virtual void undo() override { for (int i=commands_.size()-1;i>=0;i--) { commands_.at(i)->undo(); } } private: Sequence* timeline_; TimeRangeList regions_; QVector commands_; }; class WorkareaSetEnabledCommand : public UndoCommand { public: WorkareaSetEnabledCommand(Project *project, TimelinePoints* points, bool enabled) : project_(project), points_(points), old_enabled_(points_->workarea()->enabled()), new_enabled_(enabled) { } virtual Project* GetRelevantProject() const override { return project_; } virtual void redo() override { points_->workarea()->set_enabled(new_enabled_); } virtual void undo() override { points_->workarea()->set_enabled(old_enabled_); } private: Project* project_; TimelinePoints* points_; bool old_enabled_; bool new_enabled_; }; class WorkareaSetRangeCommand : public UndoCommand { public: WorkareaSetRangeCommand(Project *project, TimelinePoints* points, const TimeRange& range) : project_(project), points_(points), old_range_(points_->workarea()->range()), new_range_(range) { } virtual Project* GetRelevantProject() const override { return project_; } virtual void redo() override { points_->workarea()->set_range(new_range_); } virtual void undo() override { points_->workarea()->set_range(old_range_); } private: Project* project_; TimelinePoints* points_; TimeRange old_range_; TimeRange new_range_; }; class BlockEnableDisableCommand : public UndoCommand { public: BlockEnableDisableCommand(Block* block, bool enabled) : block_(block), old_enabled_(block_->is_enabled()), new_enabled_(enabled) { } virtual Project* GetRelevantProject() const override { return block_->project(); } virtual void redo() override { block_->set_enabled(new_enabled_); } virtual void undo() override { block_->set_enabled(old_enabled_); } private: Block* block_; bool old_enabled_; bool new_enabled_; }; class TrackSlideCommand : public UndoCommand { public: TrackSlideCommand(Track* track, const QList& moving_blocks, Block* in_adjacent, Block* out_adjacent, const rational& movement) : prepped_(false), track_(track), blocks_(moving_blocks), movement_(movement), in_adjacent_(in_adjacent), in_adjacent_remove_command_(nullptr), out_adjacent_(out_adjacent), out_adjacent_remove_command_(nullptr) { Q_ASSERT(!movement_.isNull()); } virtual ~TrackSlideCommand() override { delete in_adjacent_remove_command_; delete out_adjacent_remove_command_; } virtual Project* GetRelevantProject() const override { return track_->project(); } virtual void redo() override { if (!prepped_) { prep(); prepped_ = true; } // Make sure all movement blocks' old positions are invalidated TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); track_->BeginOperation(); // We will always have an in adjacent if there was a valid slide if (we_created_in_adjacent_) { // We created in adjacent, so all we have to do is insert it in_adjacent_->setParent(track_->parent()); track_->InsertBlockBefore(in_adjacent_, blocks_.first()); } else if (-movement_ == in_adjacent_->length()) { // Movement will remove in adjacent track_->RippleRemoveBlock(in_adjacent_); if (NodeCanBeRemoved(in_adjacent_)) { if (!in_adjacent_remove_command_) { in_adjacent_remove_command_ = CreateRemoveCommand(in_adjacent_); } in_adjacent_remove_command_->redo(); } } else { // Simply resize adjacent in_adjacent_->set_length_and_media_out(in_adjacent_->length() + movement_); } // We may not have an out adjacent if the slide was at the end of the track if (out_adjacent_) { if (we_created_out_adjacent_) { // We created out adjacent, so we just have to insert it out_adjacent_->setParent(track_->parent()); track_->InsertBlockAfter(out_adjacent_, blocks_.last()); } else if (movement_ == out_adjacent_->length()) { // Movement will remove out adjacent track_->RippleRemoveBlock(out_adjacent_); if (NodeCanBeRemoved(out_adjacent_)) { if (!out_adjacent_remove_command_) { out_adjacent_remove_command_ = CreateRemoveCommand(out_adjacent_); } out_adjacent_remove_command_->redo(); } } else { // Simply resize 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_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); } virtual void undo() override { // Make sure all movement blocks' old positions are invalidated TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); track_->BeginOperation(); if (we_created_in_adjacent_) { // We created this, so we can remove it now track_->RippleRemoveBlock(in_adjacent_); in_adjacent_->setParent(&memory_manager_); } else if (in_adjacent_remove_command_) { // We removed this, so we can restore it now in_adjacent_remove_command_->undo(); } else { // Simply resize adjacent in_adjacent_->set_length_and_media_out(in_adjacent_->length() - movement_); } if (out_adjacent_) { if (we_created_out_adjacent_) { // We created this, so we can remove it now track_->RippleRemoveBlock(out_adjacent_); out_adjacent_->setParent(&memory_manager_); } else if (out_adjacent_remove_command_) { out_adjacent_remove_command_->undo(); } else { 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_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); } private: bool prepped_; void prep() { if (!in_adjacent_) { in_adjacent_ = new GapBlock(); in_adjacent_->set_length_and_media_out(movement_); in_adjacent_->setParent(&memory_manager_); we_created_in_adjacent_ = true; } else { we_created_in_adjacent_ = false; } if (!out_adjacent_) { if (blocks_.last()->next()) { out_adjacent_ = new GapBlock(); out_adjacent_->set_length_and_media_out(-movement_); out_adjacent_->setParent(&memory_manager_); we_created_out_adjacent_ = true; } else { we_created_out_adjacent_ = false; } } } Track* track_; QList blocks_; rational movement_; bool we_created_in_adjacent_; Block* in_adjacent_; UndoCommand* in_adjacent_remove_command_; bool we_created_out_adjacent_; Block* out_adjacent_; UndoCommand* out_adjacent_remove_command_; QObject memory_manager_; }; class TrackListInsertGaps : public UndoCommand { public: TrackListInsertGaps(TrackList* track_list, const rational& point, const rational& length) : prepped_(false), track_list_(track_list), point_(point), length_(length), split_command_(nullptr) { } virtual ~TrackListInsertGaps() override { delete split_command_; } virtual Project* GetRelevantProject() const override { return track_list_->parent()->project(); } virtual void redo() override { if (!prepped_) { prep(); prepped_ = true; } if (all_tracks_unlocked_) { // Optimize by shifting over since we have a constant amount of time being inserted if (track_list_->type() == Track::kVideo) { track_list_->parent()->ShiftVideoCache(point_, point_ + length_); } else if (track_list_->type() == Track::kAudio) { track_list_->parent()->ShiftAudioCache(point_, point_ + length_); } } foreach (Track* track, working_tracks_) { track->BeginOperation(); } foreach (Block* gap, gaps_to_extend_) { gap->set_length_and_media_out(gap->length() + length_); } if (split_command_) { split_command_->redo(); } foreach (auto add_gap, gaps_added_) { add_gap.gap->setParent(add_gap.track->parent()); add_gap.track->InsertBlockAfter(add_gap.gap, add_gap.before); } foreach (Track* track, working_tracks_) { track->EndOperation(); } if (!all_tracks_unlocked_) { foreach (Track* track, working_tracks_) { track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); } } } virtual void undo() override { if (all_tracks_unlocked_) { // Optimize by shifting over since we have a constant amount of time being inserted if (track_list_->type() == Track::kVideo) { track_list_->parent()->ShiftVideoCache(point_ + length_, point_); } else if (track_list_->type() == Track::kAudio) { track_list_->parent()->ShiftAudioCache(point_ + length_, point_); } } foreach (Track* track, working_tracks_) { track->BeginOperation(); } // Remove added gaps foreach (auto add_gap, gaps_added_) { add_gap.gap->track()->RippleRemoveBlock(add_gap.gap); add_gap.gap->setParent(&memory_manager_); } // Un-split blocks if (split_command_) { split_command_->undo(); } // Restore original length of gaps foreach (Block* gap, gaps_to_extend_) { gap->set_length_and_media_out(gap->length() - length_); } foreach (Track* track, working_tracks_) { track->EndOperation(); } if (!all_tracks_unlocked_) { foreach (Track* track, working_tracks_) { track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); } } } private: bool prepped_; void prep() { // Determine if all tracks will be affected, which will allow us to make some optimizations all_tracks_unlocked_ = true; foreach (Track* track, track_list_->GetTracks()) { if (track->IsLocked()) { all_tracks_unlocked_ = false; continue; } working_tracks_.append(track); } QVector blocks_to_split; QVector blocks_to_append_gap_to; QVector tracks_to_append_gap_to; foreach (Track* track, working_tracks_) { foreach (Block* b, track->Blocks()) { if (dynamic_cast(b) && b->in() <= point_ && b->out() >= point_) { // Found a gap at the location gaps_to_extend_.append(b); break; } else if (dynamic_cast(b) && b->out() >= point_) { bool append_gap = true; if (b->in() == point_) { // The only reason we should be here is if this block is at the start of the track, // in which case no split needs to occur b = nullptr; } else if (b->out() > point_) { // Block must be split as well as having a gap appended to it blocks_to_split.append(b); } else if (!b->next()) { // At the end of a track, no gap needs to be added at all append_gap = false; } if (append_gap) { tracks_to_append_gap_to.append(track); blocks_to_append_gap_to.append(b); } break; } } } if (!blocks_to_split.isEmpty()) { split_command_ = new BlockSplitPreservingLinksCommand(blocks_to_split, {point_}); } for (int i=0; iset_length_and_media_out(length_); gap->setParent(&memory_manager_); gaps_added_.append({gap, blocks_to_append_gap_to.at(i), tracks_to_append_gap_to.at(i)}); } } TrackList* track_list_; rational point_; rational length_; QVector working_tracks_; bool all_tracks_unlocked_; QVector gaps_to_extend_; struct AddGap { GapBlock* gap; Block* before; Track* track; }; QVector gaps_added_; BlockSplitPreservingLinksCommand* split_command_; QObject memory_manager_; }; } #endif // TIMELINEUNDOABLE_H