diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 85b92530b..09289ac8d 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -31,6 +31,7 @@ Block::Block() : previous_(nullptr), next_(nullptr), track_(nullptr), + index_(-1), in_transition_(nullptr), out_transition_(nullptr) { diff --git a/app/node/block/block.h b/app/node/block/block.h index bf597c001..53bc42a31 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -165,6 +165,16 @@ public: out_transition_ = t; } + int index() const + { + return index_; + } + + void set_index(int i) + { + index_ = i; + } + virtual void Hash(QCryptographicHash &hash, const rational &time) const override; public slots: @@ -201,6 +211,7 @@ private: rational in_point_; rational out_point_; Track* track_; + int index_; TransitionBlock* in_transition_; TransitionBlock* out_transition_; diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 5870b602a..eec21094b 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -286,7 +286,8 @@ void Track::InvalidateCache(const TimeRange& range, const InputConnection& from) limited = TimeRange(qMax(range.in(), b->in()), qMin(range.out(), b->out())); } else { - limited = TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), track_length())); + limited = TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), last_invalidated_length_)); + last_invalidated_length_ = track_length(); } Node::InvalidateCache(limited, from); @@ -294,19 +295,27 @@ void Track::InvalidateCache(const TimeRange& range, const InputConnection& from) void Track::InsertBlockBefore(Block* block, Block* after) { - InsertBlockAtIndex(block, blocks_.indexOf(after)); + if (!after) { + AppendBlock(block); + } else { + InsertBlockAtIndex(block, blocks_.indexOf(after)); + } } void Track::InsertBlockAfter(Block *block, Block *before) { - int before_index = blocks_.indexOf(before); - - Q_ASSERT(before_index >= 0); - - if (before_index == blocks_.size() - 1) { - AppendBlock(block); + if (!before) { + PrependBlock(block); } else { - InsertBlockAtIndex(block, before_index + 1); + int before_index = blocks_.indexOf(before); + + Q_ASSERT(before_index >= 0); + + if (before_index == blocks_.size() - 1) { + AppendBlock(block); + } else { + InsertBlockAtIndex(block, before_index + 1); + } } } @@ -320,7 +329,7 @@ void Track::PrependBlock(Block *block) EndOperation(); // Everything has shifted at this point - Node::InvalidateCache(TimeRange(0, track_length()), InputConnection()); + InvalidateCache(TimeRange(0, track_length())); } void Track::InsertBlockAtIndex(Block *block, int index) @@ -333,7 +342,7 @@ void Track::InsertBlockAtIndex(Block *block, int index) EndOperation(); - Node::InvalidateCache(TimeRange(block->in(), track_length())); + InvalidateCache(TimeRange(block->in(), track_length())); } void Track::AppendBlock(Block *block) @@ -346,7 +355,7 @@ void Track::AppendBlock(Block *block) EndOperation(); // Invalidate area that block was added to - Node::InvalidateCache(TimeRange(block->in(), track_length())); + InvalidateCache(TimeRange(block->in(), track_length())); } void Track::RippleRemoveBlock(Block *block) @@ -360,7 +369,7 @@ void Track::RippleRemoveBlock(Block *block) EndOperation(); - Node::InvalidateCache(TimeRange(remove_in, qMax(track_length(), remove_out))); + InvalidateCache(TimeRange(remove_in, qMax(track_length(), remove_out))); } void Track::ReplaceBlock(Block *old, Block *replace) @@ -376,9 +385,9 @@ void Track::ReplaceBlock(Block *old, Block *replace) EndOperation(); if (old->length() == replace->length()) { - Node::InvalidateCache(TimeRange(replace->in(), replace->out())); + InvalidateCache(TimeRange(replace->in(), replace->out())); } else { - Node::InvalidateCache(TimeRange(replace->in(), RATIONAL_MAX)); + InvalidateCache(TimeRange(replace->in(), RATIONAL_MAX)); } } @@ -432,7 +441,7 @@ void Track::Hash(QCryptographicHash &hash, const rational &time) const void Track::SetMuted(bool e) { muted_input_->SetStandardValue(e); - Node::InvalidateCache(TimeRange(0, track_length())); + InvalidateCache(TimeRange(0, track_length())); } void Track::SetLocked(bool e) @@ -454,6 +463,8 @@ void Track::UpdateInOutFrom(int index) last_out += b->length(); b->set_out(last_out); + + b->set_index(i); } emit BlocksRefreshed(); @@ -480,13 +491,16 @@ int Track::GetCacheIndexFromArrayIndex(int index) const void Track::SetLengthInternal(const rational &r, bool invalidate) { if (r != track_length_) { + // TimeRange will automatically normalize so that the shorter number is the in and the longer + // is the out TimeRange invalidate_range(track_length_, r); track_length_ = r; + last_invalidated_length_ = qMax(last_invalidated_length_, track_length_); emit TrackLengthChanged(); if (invalidate) { - Node::InvalidateCache(invalidate_range); + InvalidateCache(invalidate_range); } } } @@ -557,7 +571,7 @@ void Track::BlockConnected(Node *node, int element) connect(block, &Block::LengthChanged, this, &Track::BlockLengthChanged); // Invalidate cache now that block should have an in point - Node::InvalidateCache(TimeRange(block->in(), track_length())); + InvalidateCache(TimeRange(block->in(), track_length())); // Emit block added signal emit BlockAdded(block); @@ -615,7 +629,7 @@ void Track::BlockDisconnected(Node* node, int element) disconnect(b, &Block::LengthChanged, this, &Track::BlockLengthChanged); - Node::InvalidateCache(invalidate_range); + InvalidateCache(invalidate_range); } void Track::BlockLengthChanged() @@ -631,7 +645,7 @@ void Track::BlockLengthChanged() TimeRange invalidate_region(qMin(old_out, new_out), track_length()); - Node::InvalidateCache(invalidate_region); + InvalidateCache(invalidate_region); } void Track::MutedInputValueChanged() diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index c8609a9ed..7eedcd6a1 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -220,7 +220,7 @@ public: return blocks_; } - virtual void InvalidateCache(const TimeRange& range, const InputConnection& from) override; + virtual void InvalidateCache(const TimeRange& range, const InputConnection& from = InputConnection()) override; /** * @brief Adds Block `block` at the very beginning of the Sequence before all other clips @@ -358,6 +358,8 @@ private: rational track_length_; + rational last_invalidated_length_; + double track_height_; int index_; diff --git a/app/widget/timelinewidget/CMakeLists.txt b/app/widget/timelinewidget/CMakeLists.txt index 62e276ba9..cda37359d 100644 --- a/app/widget/timelinewidget/CMakeLists.txt +++ b/app/widget/timelinewidget/CMakeLists.txt @@ -22,7 +22,6 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} widget/timelinewidget/timelineandtrackview.cpp widget/timelinewidget/timelineandtrackview.h - widget/timelinewidget/timelineundo.cpp widget/timelinewidget/timelineundo.h widget/timelinewidget/timelinewidget.cpp widget/timelinewidget/timelinewidget.h diff --git a/app/widget/timelinewidget/timelineundo.cpp b/app/widget/timelinewidget/timelineundo.cpp deleted file mode 100644 index ecc3888ca..000000000 --- a/app/widget/timelinewidget/timelineundo.cpp +++ /dev/null @@ -1,1664 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2020 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 "timelineundo.h" - -#include "config/config.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" -#include "widget/timelinewidget/timelinewidget.h" - -namespace olive { - -TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand(Track *track, rational in, rational out, QUndoCommand *parent) : - UndoCommand(parent), - track_(track), - in_(in), - out_(out), - splice_(false), - splice_split_command_(nullptr), - 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_) { - - if (trim_out_->type() == Block::kGap && !insert_) { - - // Gaps shouldn't be split, just trim the difference - trim_out_->set_length_and_media_out(trim_out_->length() - (out_ - in_)); - - } else { - - // Split the block here - splice_split_command_ = new QUndoCommand(); - - if (Config::Current()[QStringLiteral("SplitClipsCopyNodes")].toBool()) { - QVector nodes_to_clone; - nodes_to_clone.append(trim_out_); - nodes_to_clone.append(trim_out_->GetDependencies()); - QVector duplicated = Node::CopyDependencyGraph(nodes_to_clone, splice_split_command_); - trim_in_ = static_cast(duplicated.first()); - } else { - trim_in_ = static_cast(trim_out_->copy()); - new NodeAddCommand(static_cast(track_->parent()), trim_in_, splice_split_command_); - new NodeCopyInputsCommand(trim_out_, trim_in_, true, splice_split_command_); - } - - splice_split_command_->redo(); - - 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); - - QUndoCommand* remove_command = new QUndoCommand(); - Node::RemoveNodesAndExclusiveDependencies(remove_block, remove_command); - 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_->Node::InvalidateCache(TimeRange(in_, insert_ ? out_ : RATIONAL_MAX), - 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_) { - - if (trim_out_->type() == Block::kGap && !insert_) { - - // Just restore the length - trim_out_->set_length_and_media_out(trim_out_->length() + (out_ - in_)); - - } else { - - // 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_); - - splice_split_command_->undo(); - - } - - } 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--) { - QUndoCommand* 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_->Node::InvalidateCache(TimeRange(in_, insert_ ? out_ : RATIONAL_MAX), - track_->block_input()); - - if (splice_split_command_) { - delete splice_split_command_; - splice_split_command_ = nullptr; - } -} - -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() -{ - // Determine if we need to add tracks - if (track_index_ >= timeline_->GetTracks().size()) { - if (added_tracks_.isEmpty()) { - // First redo, create tracks now - added_tracks_.resize(track_index_ - timeline_->GetTracks().size() + 1); - - for (int i=0; isetParent(timeline_->GetParentGraph()); - - timeline_->track_input()->ArrayAppend(); - Node::ConnectEdge(track, timeline_->track_input(), timeline_->track_input()->ArraySize() - 1); - } - } - - 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()); - gap_->setParent(track_->parent()); - 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_); - gap_->setParent(&memory_manager_); - } - } else { - TrackRippleRemoveAreaCommand::undo_internal(); - } - - for (int i=added_tracks_.size()-1; i>=0; i--) { - Track* track = added_tracks_.at(i); - Node::DisconnectEdge(track, timeline_->track_input(), timeline_->track_input()->ArraySize() - 1); - track->setParent(&memory_manager_); - timeline_->track_input()->ArrayRemoveLast(); - } -} - -BlockSplitCommand::BlockSplitCommand(Track* 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 = block_->out_transition(); - 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(); - - NodeGraph* graph = block_->parent(); - - add_command_ = new QUndoCommand(); - new NodeAddCommand(graph, new_block_, add_command_); - - bool copy_dependencies_too = Config::Current()[QStringLiteral("SplitClipsCopyNodes")].toBool(); - - new NodeCopyInputsCommand(block_, new_block_, !copy_dependencies_too, add_command_); - - if (copy_dependencies_too) { - - QVector src_nodes; - QVector dst_nodes; - - src_nodes.append(block_); - src_nodes.append(block_->GetDependencies()); - - dst_nodes.resize(src_nodes.size()); - dst_nodes[0] = new_block_; - for (int i=1; icopy(); - new NodeAddCommand(graph, dst_nodes[i], add_command_); - Node::CopyInputs(src_nodes[i], dst_nodes[i], false); - } - - Node::CopyDependencyGraph(src_nodes, dst_nodes, add_command_); - - } - - add_command_->redo(); - - 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_) { - Node::DisconnectEdge(block_, transition); - Node::ConnectEdge(new_block_, transition); - } - - track_->EndOperation(); -} - -void BlockSplitCommand::undo_internal() -{ - track_->BeginOperation(); - - foreach (NodeInput* transition, transitions_to_move_) { - Node::DisconnectEdge(new_block_, transition); - Node::ConnectEdge(block_, transition); - } - - block_->set_length_and_media_out(old_length_); - track_->RippleRemoveBlock(new_block_); - - add_command_->undo(); - new_block_->setParent(&memory_manager_); - delete add_command_; - - track_->EndOperation(); -} - -Block *BlockSplitCommand::new_block() -{ - return new_block_; -} - -TrackSplitAtTimeCommand::TrackSplitAtTimeCommand(Track *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(Track* 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(Track *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) { - BlockSplitCommand* split_command = new BlockSplitCommand(b->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 (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 (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(resize->track(), 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 QVector 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(Track* 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); - adjacent_->setParent(track_->parent()); - - 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_); - adjacent_->setParent(&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_->Node::InvalidateCache(invalidate_range, 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_); - adjacent_->setParent(&memory_manager_); - 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_) { - adjacent_->setParent(track_->parent()); - - 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_->Node::InvalidateCache(invalidate_range, track_->block_input()); -} - -TrackReplaceBlockWithGapCommand::TrackReplaceBlockWithGapCommand(Track *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_); - existing_merged_gap_->setParent(&memory_manager_); - } else if (previous_is_a_gap) { - // Extend this gap to fill space left by block - existing_gap_ = static_cast(previous); - } else if (next_is_a_gap) { - // Extend this gap to fill space left by block - existing_gap_ = static_cast(next); - } - - if (existing_gap_) { - // Extend an existing gap - new_gap_length += existing_gap_->length(); - existing_gap_->set_length_and_media_out(new_gap_length); - track_->RippleRemoveBlock(block_); - - existing_gap_precedes_ = (existing_gap_ == previous); - } else { - // No gap exists to fill this space, create a new one and swap it in - our_gap_ = new GapBlock(); - our_gap_->set_length_and_media_out(new_gap_length); - our_gap_->setParent(track_->parent()); - 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); - preceding->setParent(&memory_manager_); - - existing_merged_gap_ = static_cast(preceding); - } - - // Remove block in question - track_->RippleRemoveBlock(block_); - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(invalidate_range, 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_); - our_gap_->setParent(&memory_manager_); - 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(); - existing_merged_gap_->setParent(track_->parent()); - track_->InsertBlockAfter(existing_merged_gap_, existing_gap_); - existing_merged_gap_ = nullptr; - } - - // Restore original block - if (existing_gap_precedes_) { - track_->InsertBlockAfter(block_, existing_gap_); - } else { - track_->InsertBlockBefore(block_, existing_gap_); - } - - // Restore gap's original length - existing_gap_->set_length_and_media_out(original_gap_length); - - existing_gap_ = nullptr; - - } else { - - // Our gap and existing gap were both null, our block must have been at the end and thus - // required no gap extension/replacement - - // However, we may have removed an unnecessary gap that preceded it - if (existing_merged_gap_) { - existing_merged_gap_->setParent(track_->parent()); - track_->AppendBlock(existing_merged_gap_); - existing_merged_gap_ = nullptr; - } - - // Restore block - track_->AppendBlock(block_); - - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), track_->block_input()); -} - -TrackSlideCommand::TrackSlideCommand(Track* 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_); - in_adjacent_->setParent(&memory_manager_); - 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 - in_adjacent_->setParent(track_->parent()); - 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_); - out_adjacent_->setParent(&memory_manager_); - 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 - out_adjacent_->setParent(track_->parent()); - 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_); - gap->setParent(track_->parent()); - 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_); - in_adjacent_->setParent(&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_); - gap->setParent(track_->parent()); - 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_); - out_adjacent_->setParent(&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_->Node::InvalidateCache(invalidate_range, 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 (Track* 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() == Track::kVideo) { - static_cast(list_->parent())->ShiftVideoCache(out_, in_); - } else if (list_->type() == Track::kAudio) { - static_cast(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(); - } - } -} - -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() == Track::kVideo) { - static_cast(list_->parent())->ShiftVideoCache(in_, out_); - } else if (list_->type() == Track::kAudio) { - static_cast(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(); - } - } -} - -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); - b->setParent(&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); - gap->setParent(info.ref_block->parent()); - 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() == Track::kVideo) { - static_cast(track_list_->parent())->ShiftVideoCache(old_latest_pt, new_latest_pt); - } else if (track_list_->type() == Track::kAudio) { - 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()); - } - } - } -} - -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; - - b->setParent(info.track->parent()); - - 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); - gap->setParent(&memory_manager_); - } - } -} - -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 (Track* 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() == Track::kVideo) { - static_cast(track_list_->parent())->ShiftVideoCache(point_, point_ + length_); - } else if (track_list_->type() == Track::kAudio) { - static_cast(track_list_->parent())->ShiftAudioCache(point_, point_ + length_); - } - - foreach (Track* track, working_tracks_) { - track->BeginOperation(); - } - } - - QVector blocks_to_split; - QVector blocks_to_append_gap_to; - - foreach (Track* track, working_tracks_) { - foreach (Block* b, track->Blocks()) { - if (b->type() == Block::kGap && b->in() <= point_ && b->out() >= point_) { - // Found a gap at the location - gaps_to_extend_.append(b); - break; - } 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); - break; - } - } - } - - 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_); - gap->setParent(block->parent()); - block->track()->InsertBlockAfter(gap, block); - gaps_added_.append(gap); - } - - if (all_tracks_unlocked_) { - foreach (Track* 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() == Track::kVideo) { - static_cast(track_list_->parent())->ShiftVideoCache(point_ + length_, point_); - } else if (track_list_->type() == Track::kAudio) { - static_cast(track_list_->parent())->ShiftAudioCache(point_ + length_, point_); - } - - foreach (Track* track, working_tracks_) { - track->BeginOperation(); - } - } - - // Remove added gaps - foreach (GapBlock* gap, gaps_added_) { - gap->track()->RippleRemoveBlock(gap); - gap->setParent(&memory_manager_); - } - 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 (Track* track, working_tracks_) { - track->EndOperation(); - } - } -} - -TransitionRemoveCommand::TransitionRemoveCommand(Track* 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_) { - Node::DisconnectEdge(in_block_, block_->in_block_input()); - } - - if (out_block_) { - Node::DisconnectEdge(out_block_, block_->out_block_input()); - } - - track_->RippleRemoveBlock(block_); - - track_->EndOperation(); - - track_->Node::InvalidateCache(invalidate_range, 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_) { - Node::ConnectEdge(in_block_, block_->in_block_input()); - } - - if (out_block_) { - Node::ConnectEdge(out_block_, 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_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), track_->block_input()); -} - -TimelineSetSelectionsCommand::TimelineSetSelectionsCommand(TimelineWidget *timeline, const TimelineWidgetSelections &now, const TimelineWidgetSelections &old, QUndoCommand *parent) : - QUndoCommand(parent), - timeline_(timeline), - old_(old), - now_(now) -{ -} - -void TimelineSetSelectionsCommand::redo() -{ - timeline_->SetSelections(now_); -} - -void TimelineSetSelectionsCommand::undo() -{ - timeline_->SetSelections(old_); -} - -} diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h index d56d8dc51..6fc80bfa7 100644 --- a/app/widget/timelinewidget/timelineundo.h +++ b/app/widget/timelinewidget/timelineundo.h @@ -23,17 +23,41 @@ #include +#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/graph.h" #include "node/output/track/track.h" #include "node/output/track/tracklist.h" #include "timeline/timelinepoints.h" #include "undo/undocommand.h" -#include "widget/timelinewidget/timelinewidgetselections.h" +#include "widget/nodeview/nodeviewundo.h" namespace olive { +inline bool NodeCanBeRemoved(Node* n) +{ + //return n->edges().isEmpty() && n->GetExclusiveDependencies().isEmpty(); + return n->edges().empty(); +} + +inline QUndoCommand* CreateRemoveCommand(Node* n) +{ + QUndoCommand* command = new QUndoCommand(); + Node::RemoveNodesAndExclusiveDependencies(n, command); + return command; +} + +inline QUndoCommand* CreateAndRunRemoveCommand(Node* n) +{ + QUndoCommand* command = CreateRemoveCommand(n); + command->redo(); + return command; +} + class BlockResizeCommand : public UndoCommand { public: BlockResizeCommand(Block* block, rational new_length, QUndoCommand* parent = nullptr) : @@ -99,22 +123,233 @@ private: 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, QUndoCommand* command = nullptr); + BlockTrimCommand(Track *track, Block* block, rational new_length, Timeline::MovementMode mode, QUndoCommand* command = nullptr) : + UndoCommand(command), + prepped_(false), + track_(track), + block_(block), + new_length_(new_length), + mode_(mode), + deleted_adjacent_command_(nullptr), + trim_is_a_roll_edit_(false) + { + } - virtual Project* GetRelevantProject() const override; + virtual ~BlockTrimCommand() override + { + delete deleted_adjacent_command_; + } + virtual Project* GetRelevantProject() const override + { + return track_->parent()->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; + } + protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + if (!prepped_) { + prep(); + prepped_ = true; + } + + if (doing_nothing_) { + return; + } + + // Begin an operation since we'll be doing a lot + track_->BeginOperation(); + + // Determine how much time to invalidate + TimeRange invalidate_range; + + if (mode_ == Timeline::kTrimIn) { + invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff_); + block_->set_length_and_media_in(new_length_); + } else { + invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff_); + block_->set_length_and_media_out(new_length_); + } + + if (needs_adjacent_) { + if (we_created_adjacent_) { + // Add adjacent and insert it + adjacent_->setParent(track_->parent()); + + if (mode_ == Timeline::kTrimIn) { + track_->InsertBlockBefore(adjacent_, block_); + } else { + track_->InsertBlockAfter(adjacent_, block_); + } + } else if (we_removed_adjacent_) { + track_->RippleRemoveBlock(adjacent_); + + // It no longer inputs/outputs anything, remove it + if (remove_block_from_graph_ && NodeCanBeRemoved(adjacent_)) { + if (!deleted_adjacent_command_) { + deleted_adjacent_command_ = CreateAndRunRemoveCommand(adjacent_); + } else { + deleted_adjacent_command_->redo(); + } + } + } else { + rational adjacent_length = adjacent_->length() + trim_diff_; + + 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 = block_->range(); + } + + track_->InvalidateCache(invalidate_range, track_->block_input()); + } + + virtual void undo_internal() override + { + if (doing_nothing_) { + return; + } + + track_->BeginOperation(); + + // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer + if (needs_adjacent_) { + if (we_created_adjacent_) { + // Adjacent is ours, just delete it + track_->RippleRemoveBlock(adjacent_); + adjacent_->setParent(&memory_manager_); + } else { + if (we_removed_adjacent_) { + if (deleted_adjacent_command_) { + // We deleted adjacent, restore it now + deleted_adjacent_command_->undo(); + } + + if (mode_ == Timeline::kTrimIn) { + track_->InsertBlockBefore(adjacent_, block_); + } else { + track_->InsertBlockAfter(adjacent_, block_); + } + } else { + rational adjacent_length = adjacent_->length() - trim_diff_; + + if (mode_ == Timeline::kTrimIn) { + adjacent_->set_length_and_media_out(adjacent_length); + } else { + adjacent_->set_length_and_media_in(adjacent_length); + } + } + } + } + + TimeRange invalidate_range; + + if (mode_ == Timeline::kTrimIn) { + block_->set_length_and_media_in(old_length_); + + invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff_); + } else { + block_->set_length_and_media_out(old_length_); + + invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff_); + } + + if (block_->type() == Block::kTransition) { + // Whole transition needs to be invalidated + invalidate_range = block_->range(); + } + + track_->EndOperation(); + + track_->InvalidateCache(invalidate_range, track_->block_input()); + } private: + void prep() + { + // Store old length + old_length_ = block_->length(); + + // Determine if the length isn't changing, in which case we set a flag to do nothing + if ((doing_nothing_ = (old_length_ == new_length_))) { + return; + } + + // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer + trim_diff_ = old_length_ - new_length_; + + // Retrieve our adjacent block (or nullptr if none) + if (mode_ == Timeline::kTrimIn) { + adjacent_ = block_->previous(); + } else { + adjacent_ = block_->next(); + } + + // Ignore when trimming the out with no adjacent, because the user must have trimmed the end + // of the last block in the track, so we don't need to do anything elses + needs_adjacent_ = (mode_ == Timeline::kTrimIn || adjacent_); + + if (needs_adjacent_) { + // If we're trimming shorter, we need an adjacent, so check if we have a viable one. + we_created_adjacent_ = (trim_diff_ > 0 && (!adjacent_ || (adjacent_->type() != Block::kGap && !trim_is_a_roll_edit_))); + + if (we_created_adjacent_) { + // We shortened but don't have a viable adjacent to lengthen, so we create one + adjacent_ = new GapBlock(); + adjacent_->set_length_and_media_out(trim_diff_); + } else { + // Determine if we're removing the adjacent + rational adjacent_length = adjacent_->length() + trim_diff_; + we_removed_adjacent_ = adjacent_length.isNull(); + } + } + } + + bool prepped_; + bool doing_nothing_; + rational trim_diff_; + Track* track_; Block* block_; rational old_length_; @@ -122,10 +357,13 @@ private: Timeline::MovementMode mode_; Block* adjacent_; + bool needs_adjacent_; bool we_created_adjacent_; - bool we_deleted_adjacent_; + bool we_removed_adjacent_; + QUndoCommand* deleted_adjacent_command_; bool trim_is_a_roll_edit_; + bool remove_block_from_graph_; QObject memory_manager_; @@ -186,11 +424,7 @@ protected: virtual void undo_internal() override { - if (before_) { - track_->InsertBlockAfter(block_, before_); - } else { - track_->PrependBlock(block_); - } + track_->InsertBlockAfter(block_, before_); } private: @@ -204,13 +438,28 @@ private: class TrackPrependBlockCommand : public UndoCommand { public: - TrackPrependBlockCommand(Track* track, Block* block, QUndoCommand* parent = nullptr); + TrackPrependBlockCommand(Track* track, Block* block, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + track_(track), + block_(block) + { + } - virtual Project* GetRelevantProject() const override; + virtual Project* GetRelevantProject() const override + { + return track_->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + track_->PrependBlock(block_); + } + + virtual void undo_internal() override + { + track_->RippleRemoveBlock(block_); + } private: Track* track_; @@ -251,6 +500,430 @@ private: Block* before_; }; +class BlockLinkCommand : public UndoCommand { +public: + BlockLinkCommand(Block* a, Block* b, bool link, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + a_(a), + b_(b), + link_(link) + { + } + + virtual Project* GetRelevantProject() const override + { + return a_->parent()->project(); + } + +protected: + virtual void redo_internal() override + { + if (link_) { + done_ = Block::Link(a_, b_); + } else { + done_ = Block::Unlink(a_, b_); + } + } + + virtual void undo_internal() override + { + if (done_) { + if (link_) { + Block::Unlink(a_, b_); + } else { + Block::Link(a_, b_); + } + } + } + +private: + Block* a_; + + Block* b_; + + bool link_; + + bool done_; + +}; + +class BlockUnlinkAllCommand : public UndoCommand { +public: + BlockUnlinkAllCommand(Block* block, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + block_(block) + { + } + + virtual Project* GetRelevantProject() const override + { + return static_cast(block_->parent())->project(); + } + +protected: + virtual void redo_internal() override + { + unlinked_ = block_->linked_clips(); + + foreach (Block* link, unlinked_) { + Block::Unlink(block_, link); + } + } + + virtual void undo_internal() override + { + foreach (Block* link, unlinked_) { + Block::Link(block_, link); + } + + unlinked_.clear(); + } + +private: + Block* block_; + + QVector unlinked_; + +}; + +class BlockLinkManyCommand : public UndoCommand { +public: + BlockLinkManyCommand(const QVector blocks, bool link, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + blocks_(blocks) + { + foreach (Block* a, blocks_) { + foreach (Block* b, blocks_) { + if (a != b) { + new BlockLinkCommand(a, b, link, this); + } + } + } + } + + virtual Project* GetRelevantProject() const override + { + return blocks_.first()->parent()->project(); + } + +private: + QVector blocks_; + +}; + +class BlockSplitCommand : public UndoCommand { +public: + BlockSplitCommand(Block* block, rational point, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + block_(block), + point_(point), + reconnect_tree_command_(nullptr) + { + } + + virtual ~BlockSplitCommand() override + { + delete reconnect_tree_command_; + } + + virtual Project* GetRelevantProject() const override + { + return block_->parent()->project(); + } + + /** + * @brief Access the second block created as a result. Only valid after redo(). + */ + Block* new_block() + { + return static_cast(added_nodes_.first()); + } + +protected: + virtual void redo_internal() override + { + old_length_ = block_->length(); + + Q_ASSERT(block_->type() == Block::kClip && point_ > block_->in() && point_ < block_->out()); + + // Determine if we're copying the dependencies as part of this command + bool copy_dependencies_too = Config::Current()[QStringLiteral("SplitClipsCopyNodes")].toBool(); + + // Copy nodes if we haven't already done it + if (added_nodes_.isEmpty()) { + if (copy_dependencies_too) { + src_nodes_.append(block_); + src_nodes_.append(block_->GetDependencies()); + + added_nodes_.resize(src_nodes_.size()); + for (int i=0; icopy(); + + // Add copy to graph + copy->setParent(block_->parent()); + + // Copy inputs + Node::CopyInputs(src_nodes_[i], copy, false); + + // Keep in array + added_nodes_[i] = copy; + + } + } else { + // Just copy the block itself + added_nodes_.append(block_->copy()); + Node::CopyInputs(block_, added_nodes_.first(), true); + } + } + + // Add all new nodes to the graph + foreach (Node* n, added_nodes_) { + n->setParent(block_->parent()); + } + + if (copy_dependencies_too) { + // Create equivalent connections among our copied dependency tree + if (!reconnect_tree_command_) { + reconnect_tree_command_ = new QUndoCommand(); + Node::CopyDependencyGraph(src_nodes_, added_nodes_, 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_); + + // If the block had an out transition, we move it to the new block + moved_transition_ = nullptr; + + TransitionBlock* potential_transition = dynamic_cast(new_block()->next()); + if (potential_transition) { + foreach (const Node::InputConnection& conn, block_->edges()) { + if (conn.input->parent() == potential_transition) { + moved_transition_ = potential_transition->out_block_input(); + Node::DisconnectEdge(block_, moved_transition_); + Node::ConnectEdge(new_block(), moved_transition_); + break; + } + } + } + + track->EndOperation(); + } + + virtual void undo_internal() override + { + Track* track = block_->track(); + + track->BeginOperation(); + + if (moved_transition_) { + Node::DisconnectEdge(new_block(), moved_transition_); + Node::DisconnectEdge(block_, moved_transition_); + } + + block_->set_length_and_media_out(old_length_); + track->RippleRemoveBlock(new_block()); + + // If we ran a reconnect command, disconnect now + if (reconnect_tree_command_) { + reconnect_tree_command_->undo(); + } + + foreach (Node* n, added_nodes_) { + // Remove nodes + n->setParent(&memory_manager_); + } + + track->EndOperation(); + } + +private: + Block* block_; + + rational old_length_; + rational point_; + + QObject memory_manager_; + + QUndoCommand* reconnect_tree_command_; + + NodeInput* moved_transition_; + + QVector src_nodes_; + QVector added_nodes_; + +}; + +class BlockSplitPreservingLinksCommand : public UndoCommand { +public: + BlockSplitPreservingLinksCommand(const QVector &blocks, const QList& times, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + blocks_(blocks), + times_(times) + { + } + + virtual ~BlockSplitPreservingLinksCommand() override + { + qDeleteAll(commands_); + } + + virtual Project* GetRelevantProject() const override + { + return static_cast(blocks_.first()->parent())->project(); + } + +protected: + virtual void redo_internal() override + { + if (commands_.isEmpty()) { + QVector< QVector > split_blocks(times_.size()); + + for (int i=0;i times_.at(i-1)); + + QVector splits(blocks_.size()); + commands_.resize(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_.replace(j, 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) { + BlockLinkCommand* blc = new BlockLinkCommand(split_list.at(i), split_list.at(j), true); + blc->redo(); + commands_.append(blc); + } + } + } + } + } else { + for (int i=0; iredo(); + } + } + } + + virtual void undo_internal() 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, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + prepped_(false), + track_(track), + point_(point), + command_(nullptr) + { + } + + virtual ~TrackSplitAtTimeCommand() override + { + delete command_; + } + + virtual Project* GetRelevantProject() const override + { + return track_->parent()->project(); + } + +protected: + virtual void redo_internal() 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_internal() override + { + if (command_) { + command_->undo(); + } + } + +private: + bool prepped_; + + Track* track_; + + rational point_; + + QUndoCommand* command_; + +}; + /** * @brief Clears the area between in and out * @@ -260,55 +933,296 @@ private: */ class TrackRippleRemoveAreaCommand : public UndoCommand { public: - TrackRippleRemoveAreaCommand(Track* track, rational in, rational out, QUndoCommand* parent = nullptr); + TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + prepped_(false), + track_(track), + range_(range), + splice_split_command_(nullptr) + { + trim_out_.block = nullptr; + trim_in_.block = nullptr; + } - virtual Project* GetRelevantProject() const override; + virtual ~TrackRippleRemoveAreaCommand() override + { + delete splice_split_command_; + qDeleteAll(remove_block_commands_); + } - void SetInsert(Block* insert); + virtual Project* GetRelevantProject() const override + { + return track_->parent()->project(); + } + + /** + * @brief Block to insert after if you want to insert something between this ripple + */ + Block* GetInsertionIndex() const + { + return insert_before_; + } + + Block* GetSplicedBlock() const + { + if (splice_split_command_) { + return splice_split_command_->new_block(); + } + + return nullptr; + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + if (!prepped_) { + prep(); + prepped_ = true; + } -protected: - Project* project_; + 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 (QUndoCommand* c, remove_block_commands_) { + c->redo(); + } + } + } + + track_->EndOperation(); + + track_->InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX)); + } + + virtual void undo_internal() 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_->InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX)); + } + +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_trimmed = first_block->in() < range_.in(); + + insert_before_ = first_block_is_trimmed ? first_block : first_block->previous(); + + // If it's getting trimmed, determine if it's actually getting spliced + if (first_block_is_trimmed && first_block->out() > range_.out()) { + // 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_trimmed) { + trim_out_ = {first_block, + first_block->length(), + first_block->length() - (first_block->out() - range_.in())}; + } else { + removals_.append(RemoveOperation({first_block, first_block->previous()})); + } + + // 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_; - rational in_; - rational out_; + TimeRange range_; - bool splice_; - QUndoCommand* splice_split_command_; - - Block* trim_out_; - Block* trim_in_; - QVector removed_blocks_; - - rational trim_in_old_length_; - rational trim_out_old_length_; - - rational trim_in_new_length_; - rational trim_out_new_length_; - - Block* insert_; - - QObject memory_manager_; + TrimOperation trim_out_; + QVector removals_; + TrimOperation trim_in_; + Block* insert_before_; + BlockSplitCommand* splice_split_command_; QVector remove_block_commands_; }; class TrackListRippleRemoveAreaCommand : public UndoCommand { public: - TrackListRippleRemoveAreaCommand(TrackList* list, rational in, rational out, QUndoCommand* parent = nullptr); + TrackListRippleRemoveAreaCommand(TrackList* list, rational in, rational out, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + list_(list), + in_(in), + out_(out) + { + } - virtual ~TrackListRippleRemoveAreaCommand() override; + virtual ~TrackListRippleRemoveAreaCommand() override + { + qDeleteAll(commands_); + } - virtual Project* GetRelevantProject() const override; + virtual Project* GetRelevantProject() const override + { + return static_cast(list_->parent())->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() 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) { + static_cast(list_->parent())->ShiftVideoCache(out_, in_); + } else if (list_->type() == Track::kAudio) { + static_cast(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_internal() 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) { + static_cast(list_->parent())->ShiftVideoCache(in_, out_); + } else if (list_->type() == Track::kAudio) { + static_cast(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_; @@ -327,9 +1241,22 @@ private: class TimelineRippleRemoveAreaCommand : public UndoCommand { public: - TimelineRippleRemoveAreaCommand(ViewerOutput* timeline, rational in, rational out, QUndoCommand* parent = nullptr); + TimelineRippleRemoveAreaCommand(ViewerOutput* timeline, rational in, rational out, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + timeline_(timeline) + { + for (int i=0; itrack_list(static_cast(i)), + in, + out, + this); + } + } - virtual Project* GetRelevantProject() const override; + virtual Project* GetRelevantProject() const override + { + return timeline_->parent()->project(); + } private: ViewerOutput* timeline_; @@ -349,13 +1276,163 @@ public: TrackListRippleToolCommand(TrackList* track_list, const QList& info, const Timeline::MovementMode& movement_mode, - QUndoCommand* parent = nullptr); + QUndoCommand* parent = nullptr) : + UndoCommand(parent), + track_list_(track_list), + info_(info), + movement_mode_(movement_mode) + { + working_data_.resize(info_.size()); - virtual Project* GetRelevantProject() const override; + all_tracks_unlocked_ = (info_.size() == track_list_->GetTrackCount()); + } + + virtual Project* GetRelevantProject() const override + { + return static_cast(track_list_->parent())->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + 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); + b->setParent(&memory_manager_); + } + } else if (info.new_length > 0) { + // This is a gap we are creating + GapBlock* gap = working_data_[i].created_gap; + + if (!gap) { + gap = new GapBlock(); + working_data_[i].created_gap = gap; + } + + gap->set_length_and_media_out(info.new_length); + gap->setParent(info.ref_block->parent()); + + 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() == Track::kVideo) { + static_cast(track_list_->parent())->ShiftVideoCache(old_latest_pt, new_latest_pt); + } else if (track_list_->type() == Track::kAudio) { + 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()); + } + } + } + } + + virtual void undo_internal() override + { + // 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; + + b->setParent(info.track->parent()); + + 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); + gap->setParent(&memory_manager_); + } + } + } private: TrackList* track_list_; @@ -364,7 +1441,7 @@ private: Timeline::MovementMode movement_mode_; struct WorkingData { - GapBlock* created_gap; + GapBlock* created_gap = nullptr; Block* removed_gap_after; }; @@ -383,75 +1460,115 @@ private: * 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 TrackRippleRemoveAreaCommand { +class TrackPlaceBlockCommand : public UndoCommand { public: - TrackPlaceBlockCommand(TrackList *timeline, int track, Block* block, rational in, QUndoCommand* parent = nullptr); + TrackPlaceBlockCommand(TrackList *timeline, int track, Block* block, rational in, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + timeline_(timeline), + track_index_(track), + in_(in), + gap_(nullptr), + insert_(block), + ripple_remove_command_(nullptr) + { + } - virtual Project* GetRelevantProject() const override; + virtual ~TrackPlaceBlockCommand() override + { + delete ripple_remove_command_; + } + + virtual Project* GetRelevantProject() const override + { + return timeline_->GetParentGraph()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + // Determine if we need to add tracks + if (track_index_ >= timeline_->GetTracks().size()) { + if (added_tracks_.isEmpty()) { + // First redo, create tracks now + added_tracks_.resize(track_index_ - timeline_->GetTracks().size() + 1); + + for (int i=0; isetParent(timeline_->GetParentGraph()); + + timeline_->track_input()->ArrayAppend(); + Node::ConnectEdge(track, timeline_->track_input(), timeline_->track_input()->ArraySize() - 1); + } + } + + Track* track = timeline_->GetTrackAt(track_index_); + + 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_); + } 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()); + } + } + + virtual void undo_internal() override + { + Track* t = timeline_->GetTrackAt(track_index_); + + // Firstly, remove our insert + 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_); + } + + // Remove tracks if we added them + for (int i=added_tracks_.size()-1; i>=0; i--) { + Track* track = added_tracks_.at(i); + Node::DisconnectEdge(track, timeline_->track_input(), timeline_->track_input()->ArraySize() - 1); + track->setParent(&memory_manager_); + timeline_->track_input()->ArrayRemoveLast(); + } + } private: TrackList* timeline_; int track_index_; - bool append_; + rational in_; GapBlock* gap_; + Block* insert_; QVector added_tracks_; - -}; - -class BlockSplitCommand : public UndoCommand { -public: - BlockSplitCommand(Track* track, Block* block, rational point, QUndoCommand* parent = nullptr); - - virtual Project* GetRelevantProject() const override; - - Block* new_block(); - -protected: - virtual void redo_internal() override; - virtual void undo_internal() override; - -private: - Track* track_; - Block* block_; - Block* new_block_; - - rational new_length_; - rational old_length_; - rational point_; - - QList transitions_to_move_; - QObject memory_manager_; + TrackRippleRemoveAreaCommand* ripple_remove_command_; - QUndoCommand* add_command_; - -}; - -class TrackSplitAtTimeCommand : public UndoCommand { -public: - TrackSplitAtTimeCommand(Track* track, rational point, QUndoCommand* parent = nullptr); - - virtual Project* GetRelevantProject() const override; - -private: - Track* track_; - -}; - -class BlockSplitPreservingLinksCommand : public UndoCommand { -public: - BlockSplitPreservingLinksCommand(const QVector &blocks, const QList& times, QUndoCommand* parent = nullptr); - - virtual Project* GetRelevantProject() const override; - -private: - QVector blocks_; - - QList times_; }; /** @@ -461,29 +1578,183 @@ private: */ class TrackReplaceBlockCommand : public UndoCommand { public: - TrackReplaceBlockCommand(Track* track, Block* old, Block* replace, QUndoCommand* parent = nullptr); + TrackReplaceBlockCommand(Track* track, Block* old, Block* replace, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + track_(track), + old_(old), + replace_(replace) + { + } - virtual Project* GetRelevantProject() const override; + virtual Project* GetRelevantProject() const override + { + return track_->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + track_->ReplaceBlock(old_, replace_); + } + + virtual void undo_internal() override + { + track_->ReplaceBlock(replace_, old_); + } private: Track* track_; Block* old_; Block* replace_; + }; class TrackReplaceBlockWithGapCommand : public UndoCommand { public: - TrackReplaceBlockWithGapCommand(Track* track, Block* block, QUndoCommand* command = nullptr); + TrackReplaceBlockWithGapCommand(Track* track, Block* block, QUndoCommand* command = nullptr) : + UndoCommand(command), + track_(track), + block_(block), + existing_gap_(nullptr), + existing_merged_gap_(nullptr), + our_gap_(nullptr) + { + } - virtual Project* GetRelevantProject() const override; + virtual Project* GetRelevantProject() const override + { + return block_->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + 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_); + existing_merged_gap_->setParent(&memory_manager_); + } else if (previous_is_a_gap) { + // Extend this gap to fill space left by block + existing_gap_ = static_cast(previous); + } else if (next_is_a_gap) { + // Extend this gap to fill space left by block + existing_gap_ = static_cast(next); + } + + if (existing_gap_) { + // Extend an existing gap + new_gap_length += existing_gap_->length(); + existing_gap_->set_length_and_media_out(new_gap_length); + track_->RippleRemoveBlock(block_); + + existing_gap_precedes_ = (existing_gap_ == previous); + } else { + // No gap exists to fill this space, create a new one and swap it in + if (!our_gap_) { + our_gap_ = new GapBlock(); + our_gap_->set_length_and_media_out(new_gap_length); + } + + our_gap_->setParent(track_->parent()); + track_->ReplaceBlock(block_, our_gap_); + } + + } 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); + preceding->setParent(&memory_manager_); + + existing_merged_gap_ = static_cast(preceding); + } + + // Remove block in question + track_->RippleRemoveBlock(block_); + } + + track_->EndOperation(); + + track_->InvalidateCache(invalidate_range, track_->block_input()); + } + + virtual void undo_internal() override + { + track_->BeginOperation(); + + if (our_gap_) { + + // We made this gap, simply swap our gap back + track_->ReplaceBlock(our_gap_, block_); + our_gap_->setParent(&memory_manager_); + + } 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(); + existing_merged_gap_->setParent(track_->parent()); + track_->InsertBlockAfter(existing_merged_gap_, existing_gap_); + existing_merged_gap_ = nullptr; + } + + // Restore original block + if (existing_gap_precedes_) { + track_->InsertBlockAfter(block_, existing_gap_); + } else { + track_->InsertBlockBefore(block_, existing_gap_); + } + + // Restore gap's original length + existing_gap_->set_length_and_media_out(original_gap_length); + + existing_gap_ = nullptr; + + } else { + + // Our gap and existing gap were both null, our block must have been at the end and thus + // required no gap extension/replacement + + // However, we may have removed an unnecessary gap that preceded it + if (existing_merged_gap_) { + existing_merged_gap_->setParent(track_->parent()); + track_->AppendBlock(existing_merged_gap_); + existing_merged_gap_ = nullptr; + } + + // Restore block + track_->AppendBlock(block_); + + } + + track_->EndOperation(); + + track_->InvalidateCache(TimeRange(block_->in(), block_->out()), track_->block_input()); + } private: Track* track_; @@ -500,31 +1771,108 @@ private: class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { public: - TimelineRippleDeleteGapsAtRegionsCommand(ViewerOutput* vo, const TimeRangeList& regions, QUndoCommand* parent = nullptr); + TimelineRippleDeleteGapsAtRegionsCommand(ViewerOutput* vo, const TimeRangeList& regions, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + timeline_(vo), + regions_(regions) + { + } - virtual Project* GetRelevantProject() const override; + virtual ~TimelineRippleDeleteGapsAtRegionsCommand() override + { + qDeleteAll(commands_); + } + + virtual Project* GetRelevantProject() const override + { + return timeline_->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() 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 (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 + commands_.append(new TrackRippleRemoveBlockCommand(resize->track(), resize)); + } else { + // Resize block + commands_.append(new BlockResizeCommand(resize, resize->length() - max_ripple_length)); + } + } + } + } + } + + foreach (QUndoCommand* c, commands_) { + c->redo(); + } + } + + virtual void undo_internal() override + { + for (int i=commands_.size()-1;i>=0;i--) { + commands_.at(i)->undo(); + } + } private: ViewerOutput* timeline_; TimeRangeList regions_; - QList commands_; + QVector commands_; }; class WorkareaSetEnabledCommand : public UndoCommand { public: - WorkareaSetEnabledCommand(Project *project, TimelinePoints* points, bool enabled, QUndoCommand* parent = nullptr); + WorkareaSetEnabledCommand(Project *project, TimelinePoints* points, bool enabled, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + project_(project), + points_(points), + old_enabled_(points_->workarea()->enabled()), + new_enabled_(enabled) + { + } - virtual Project* GetRelevantProject() const override; + virtual Project* GetRelevantProject() const override + { + return project_; + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + points_->workarea()->set_enabled(new_enabled_); + } + + virtual void undo_internal() override + { + points_->workarea()->set_enabled(old_enabled_); + } private: Project* project_; @@ -539,13 +1887,30 @@ private: class WorkareaSetRangeCommand : public UndoCommand { public: - WorkareaSetRangeCommand(Project *project, TimelinePoints* points, const TimeRange& range, QUndoCommand* parent = nullptr); + WorkareaSetRangeCommand(Project *project, TimelinePoints* points, const TimeRange& range, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + project_(project), + points_(points), + old_range_(points_->workarea()->range()), + new_range_(range) + { + } - virtual Project* GetRelevantProject() const override; + virtual Project* GetRelevantProject() const override + { + return project_; + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + points_->workarea()->set_range(new_range_); + } + + virtual void undo_internal() override + { + points_->workarea()->set_range(old_range_); + } private: Project* project_; @@ -558,64 +1923,31 @@ private: }; -class BlockLinkManyCommand : public UndoCommand { -public: - BlockLinkManyCommand(const QVector blocks, bool link, QUndoCommand* parent = nullptr); - - virtual Project* GetRelevantProject() const override; - -private: - QVector blocks_; - -}; - -class BlockLinkCommand : public UndoCommand { -public: - BlockLinkCommand(Block* a, Block* b, bool link, QUndoCommand* parent = nullptr); - - virtual Project* GetRelevantProject() const override; - -protected: - virtual void redo_internal() override; - virtual void undo_internal() override; - -private: - Block* a_; - - Block* b_; - - bool link_; - - bool done_; - -}; - -class BlockUnlinkAllCommand : public UndoCommand { -public: - BlockUnlinkAllCommand(Block* block, QUndoCommand* parent = nullptr); - - virtual Project* GetRelevantProject() const override; - -protected: - virtual void redo_internal() override; - virtual void undo_internal() override; - -private: - Block* block_; - - QVector unlinked_; - -}; - class BlockEnableDisableCommand : public UndoCommand { public: - BlockEnableDisableCommand(Block* block, bool enabled, QUndoCommand* parent = nullptr); + BlockEnableDisableCommand(Block* block, bool enabled, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + block_(block), + old_enabled_(block_->is_enabled()), + new_enabled_(enabled) + { + } - virtual Project* GetRelevantProject() const override; + virtual Project* GetRelevantProject() const override + { + return block_->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() override + { + block_->set_enabled(new_enabled_); + } + + virtual void undo_internal() override + { + block_->set_enabled(old_enabled_); + } private: Block* block_; @@ -628,16 +1960,162 @@ private: class TrackSlideCommand : public UndoCommand { public: - TrackSlideCommand(Track* track, const QList& moving_blocks, Block* in_adjacent, Block* out_adjacent, const rational& movement, QUndoCommand* parent = nullptr); + TrackSlideCommand(Track* track, const QList& moving_blocks, Block* in_adjacent, Block* out_adjacent, const rational& movement, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + 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 Project* GetRelevantProject() const override; + virtual ~TrackSlideCommand() override + { + delete in_adjacent_remove_command_; + delete out_adjacent_remove_command_; + } + + virtual Project* GetRelevantProject() const override + { + return track_->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() 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_->InvalidateCache(invalidate_range, track_->block_input()); + } + + virtual void undo_internal() 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_->InvalidateCache(invalidate_range, track_->block_input()); + } private: - void slide_internal(bool undo); + 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_; @@ -645,8 +2123,10 @@ private: bool we_created_in_adjacent_; Block* in_adjacent_; + QUndoCommand* in_adjacent_remove_command_; bool we_created_out_adjacent_; Block* out_adjacent_; + QUndoCommand* out_adjacent_remove_command_; QObject memory_manager_; @@ -654,28 +2134,181 @@ private: class TrackListInsertGaps : public UndoCommand { public: - TrackListInsertGaps(TrackList* track_list, const rational& point, const rational& length, QUndoCommand* parent = nullptr); + TrackListInsertGaps(TrackList* track_list, const rational& point, const rational& length, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + prepped_(false), + track_list_(track_list), + point_(point), + length_(length), + split_command_(nullptr) + { + } - virtual Project* GetRelevantProject() const override; + virtual ~TrackListInsertGaps() override + { + delete split_command_; + } + + virtual Project* GetRelevantProject() const override + { + return static_cast(track_list_->parent())->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() 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) { + static_cast(track_list_->parent())->ShiftVideoCache(point_, point_ + length_); + } else if (track_list_->type() == Track::kAudio) { + static_cast(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.before->parent()); + add_gap.before->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->InvalidateCache(TimeRange(point_, RATIONAL_MAX)); + } + } + } + + virtual void undo_internal() 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) { + static_cast(track_list_->parent())->ShiftVideoCache(point_ + length_, point_); + } else if (track_list_->type() == Track::kAudio) { + static_cast(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->InvalidateCache(TimeRange(point_, RATIONAL_MAX)); + } + } + } 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; + + foreach (Track* track, working_tracks_) { + foreach (Block* b, track->Blocks()) { + if (b->type() == Block::kGap && b->in() <= point_ && b->out() >= point_) { + // Found a gap at the location + gaps_to_extend_.append(b); + break; + } 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); + break; + } + } + } + + 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_); + gap->setParent(&memory_manager_); + gaps_added_.append({gap, block}); + } + } + TrackList* track_list_; rational point_; rational length_; - QList working_tracks_; + QVector working_tracks_; bool all_tracks_unlocked_; - QList gaps_to_extend_; + QVector gaps_to_extend_; - QList gaps_added_; + struct AddGap { + GapBlock* gap; + Block* before; + }; + + QVector gaps_added_; BlockSplitPreservingLinksCommand* split_command_; @@ -685,41 +2318,97 @@ private: class TransitionRemoveCommand : public UndoCommand { public: - TransitionRemoveCommand(Track *track, TransitionBlock* block, QUndoCommand *parent = nullptr); + TransitionRemoveCommand(TransitionBlock* block, QUndoCommand *parent = nullptr) : + UndoCommand(parent), + block_(block) + { + } - virtual Project* GetRelevantProject() const override; + virtual Project* GetRelevantProject() const override + { + return track_->parent()->project(); + } protected: - virtual void redo_internal() override; - virtual void undo_internal() override; + virtual void redo_internal() 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_, block_->in_block_input()); + } + + if (out_block_) { + Node::DisconnectEdge(out_block_, block_->out_block_input()); + } + + track_->RippleRemoveBlock(block_); + + track_->EndOperation(); + + track_->InvalidateCache(invalidate_range, track_->block_input()); + } + + virtual void undo_internal() override + { + track_->BeginOperation(); + + if (in_block_) { + track_->InsertBlockBefore(block_, in_block_); + } else { + track_->InsertBlockAfter(block_, out_block_); + } + + if (in_block_) { + Node::ConnectEdge(in_block_, block_->in_block_input()); + } + + if (out_block_) { + Node::ConnectEdge(out_block_, 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()); + } private: - Track* track_; - TransitionBlock* block_; + Track* track_; + Block* out_block_; Block* in_block_; }; -class TimelineWidget; - -class TimelineSetSelectionsCommand : public QUndoCommand { -public: - TimelineSetSelectionsCommand(TimelineWidget* timeline, const TimelineWidgetSelections& now, const TimelineWidgetSelections& old, QUndoCommand* parent = nullptr); - -protected: - virtual void redo() override; - virtual void undo() override; - -private: - TimelineWidget* timeline_; - TimelineWidgetSelections old_; - TimelineWidgetSelections now_; - -}; - } #endif // TIMELINEUNDOABLE_H diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index f2354d419..b6ff735f3 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -469,9 +469,7 @@ void TimelineWidget::DeleteSelected(bool ripple) // For transitions, remove them but extend their attached blocks to fill their place foreach (TransitionBlock* transition, transitions_to_delete) { - new TransitionRemoveCommand(transition->track(), - transition, - command); + new TransitionRemoveCommand(transition, command); Node::RemoveNodesAndExclusiveDependencies(transition, command); } @@ -480,7 +478,7 @@ void TimelineWidget::DeleteSelected(bool ripple) ReplaceBlocksWithGaps(clips_to_delete, true, command); // Remove all selections - new TimelineSetSelectionsCommand(this, TimelineWidgetSelections(), GetSelections(), command); + new SetSelectionsCommand(this, TimelineWidgetSelections(), GetSelections(), command); // Insert ripple command now that it's all cleaned up gaps if (ripple) { diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 63cf41995..449c071c5 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -195,6 +195,34 @@ public: */ void SignalDeselectedAllBlocks(); + class SetSelectionsCommand : public QUndoCommand { + public: + SetSelectionsCommand(TimelineWidget* timeline, const TimelineWidgetSelections& now, const TimelineWidgetSelections& old, QUndoCommand* parent = nullptr) : + QUndoCommand(parent), + timeline_(timeline), + old_(old), + now_(now) + { + } + + protected: + virtual void redo() override + { + timeline_->SetSelections(now_); + } + + virtual void undo() override + { + timeline_->SetSelections(old_); + } + + private: + TimelineWidget* timeline_; + TimelineWidgetSelections old_; + TimelineWidgetSelections now_; + + }; + signals: void BlocksSelected(const QVector& selected_blocks); diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index afbe84efd..c4579aa0c 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -590,7 +590,7 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event) } else { new_sel.TrimOut(reference_ghost->GetOutAdjustment()); } - new TimelineSetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); + new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); } } @@ -655,7 +655,7 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event) TimelineWidgetSelections new_sel = parent()->GetSelections(); new_sel.ShiftTime(blocks_moving.first().ghost->GetInAdjustment()); new_sel.ShiftTracks(drag_track_type_, blocks_moving.first().ghost->GetTrackAdjustment()); - new TimelineSetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); + new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); } if (!blocks_sliding.isEmpty()) { @@ -717,7 +717,7 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event) // Adjust selections TimelineWidgetSelections new_sel = parent()->GetSelections(); new_sel.ShiftTime(movement); - new TimelineSetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); + new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); } } diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index de3c27339..8e47e19b8 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -138,7 +138,7 @@ void RippleTool::FinishDrag(TimelineViewMouseEvent *event) } else { new_sel.TrimOut(reference_ghost->GetOutAdjustment()); } - new TimelineSetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); + new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); } Core::instance()->undo_stack()->pushIfHasChildren(command);