Improves stability and cache reliability. Earlier iterations were prone to skipping necessary signals (usually leading to some sort of assert fail), particularly when track optimizations were used. Those optimizations have been moved to the viewer node so there's a higher degree of control over which signals get optimized and in which ways.
1592 lines
44 KiB
C++
1592 lines
44 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2019 Olive Team
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***/
|
|
|
|
#include "undo.h"
|
|
|
|
#include "core.h"
|
|
#include "node/block/clip/clip.h"
|
|
#include "node/block/transition/transition.h"
|
|
#include "node/graph.h"
|
|
#include "widget/nodeview/nodeviewundo.h"
|
|
|
|
OLIVE_NAMESPACE_ENTER
|
|
|
|
Node* TakeNodeFromParentGraph(Node* n, QObject* new_parent = nullptr)
|
|
{
|
|
static_cast<NodeGraph*>(n->parent())->TakeNode(n, new_parent);
|
|
|
|
return n;
|
|
}
|
|
|
|
BlockResizeCommand::BlockResizeCommand(Block *block, rational new_length, QUndoCommand* parent) :
|
|
UndoCommand(parent),
|
|
block_(block),
|
|
old_length_(block->length()),
|
|
new_length_(new_length)
|
|
{
|
|
}
|
|
|
|
Project *BlockResizeCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void BlockResizeCommand::redo_internal()
|
|
{
|
|
block_->set_length_and_media_out(new_length_);
|
|
}
|
|
|
|
void BlockResizeCommand::undo_internal()
|
|
{
|
|
block_->set_length_and_media_out(old_length_);
|
|
}
|
|
|
|
BlockResizeWithMediaInCommand::BlockResizeWithMediaInCommand(Block *block, rational new_length, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
block_(block),
|
|
old_length_(block->length()),
|
|
new_length_(new_length)
|
|
{
|
|
}
|
|
|
|
Project *BlockResizeWithMediaInCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void BlockResizeWithMediaInCommand::redo_internal()
|
|
{
|
|
block_->set_length_and_media_in(new_length_);
|
|
}
|
|
|
|
void BlockResizeWithMediaInCommand::undo_internal()
|
|
{
|
|
block_->set_length_and_media_in(old_length_);
|
|
}
|
|
|
|
BlockSetMediaInCommand::BlockSetMediaInCommand(Block *block, rational new_media_in, QUndoCommand* parent) :
|
|
UndoCommand(parent),
|
|
block_(block),
|
|
old_media_in_(block->media_in()),
|
|
new_media_in_(new_media_in)
|
|
{
|
|
}
|
|
|
|
Project *BlockSetMediaInCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void BlockSetMediaInCommand::redo_internal()
|
|
{
|
|
block_->set_media_in(new_media_in_);
|
|
}
|
|
|
|
void BlockSetMediaInCommand::undo_internal()
|
|
{
|
|
block_->set_media_in(old_media_in_);
|
|
}
|
|
|
|
TrackRippleRemoveBlockCommand::TrackRippleRemoveBlockCommand(TrackOutput *track, Block *block, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
track_(track),
|
|
block_(block)
|
|
{
|
|
}
|
|
|
|
Project *TrackRippleRemoveBlockCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void TrackRippleRemoveBlockCommand::redo_internal()
|
|
{
|
|
before_ = block_->previous();
|
|
track_->RippleRemoveBlock(block_);
|
|
}
|
|
|
|
void TrackRippleRemoveBlockCommand::undo_internal()
|
|
{
|
|
if (before_) {
|
|
track_->InsertBlockAfter(block_, before_);
|
|
} else {
|
|
track_->PrependBlock(block_);
|
|
}
|
|
}
|
|
|
|
TrackInsertBlockAfterCommand::TrackInsertBlockAfterCommand(TrackOutput *track,
|
|
Block *block,
|
|
Block *before,
|
|
QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
track_(track),
|
|
block_(block),
|
|
before_(before)
|
|
{
|
|
}
|
|
|
|
Project *TrackInsertBlockAfterCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void TrackInsertBlockAfterCommand::redo_internal()
|
|
{
|
|
track_->InsertBlockAfter(block_, before_);
|
|
}
|
|
|
|
void TrackInsertBlockAfterCommand::undo_internal()
|
|
{
|
|
track_->RippleRemoveBlock(block_);
|
|
}
|
|
|
|
TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand(TrackOutput *track, rational in, rational out, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
track_(track),
|
|
in_(in),
|
|
out_(out),
|
|
splice_(false),
|
|
trim_out_(nullptr),
|
|
trim_in_(nullptr),
|
|
insert_(nullptr)
|
|
{
|
|
}
|
|
|
|
Project *TrackRippleRemoveAreaCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(track_->parent())->project();
|
|
}
|
|
|
|
void TrackRippleRemoveAreaCommand::SetInsert(Block *insert)
|
|
{
|
|
insert_ = insert;
|
|
}
|
|
|
|
void TrackRippleRemoveAreaCommand::redo_internal()
|
|
{
|
|
// Iterate through blocks determining which need trimming/removing/splitting
|
|
foreach (Block* block, track_->Blocks()) {
|
|
if (block->in() < in_ && block->out() > out_) {
|
|
// The area entirely within this Block
|
|
trim_out_ = block;
|
|
splice_ = true;
|
|
|
|
// We don't need to do anything else here
|
|
break;
|
|
} else if (block->in() >= in_ && block->out() <= out_) {
|
|
// This Block's is entirely within the area
|
|
removed_blocks_.append(block);
|
|
} else if (block->in() < in_ && block->out() >= in_) {
|
|
// This Block's out point exceeds `in`
|
|
trim_out_ = block;
|
|
} else if (block->in() <= out_ && block->out() > out_) {
|
|
// This Block's in point exceeds `out`
|
|
trim_in_ = block;
|
|
}
|
|
}
|
|
|
|
track_->BeginOperation();
|
|
|
|
// If we picked up a block to splice
|
|
if (splice_) {
|
|
|
|
// Split the block here
|
|
trim_in_ = static_cast<Block*>(trim_out_->copy());
|
|
|
|
static_cast<NodeGraph*>(track_->parent())->AddNode(trim_in_);
|
|
Node::CopyInputs(trim_out_, trim_in_);
|
|
|
|
trim_out_old_length_ = trim_out_->length();
|
|
trim_out_->set_length_and_media_out(in_ - trim_out_->in());
|
|
|
|
trim_in_->set_length_and_media_in(trim_out_old_length_ - (out_ - trim_out_->in()));
|
|
|
|
track_->InsertBlockAfter(trim_in_, trim_out_);
|
|
|
|
} else {
|
|
|
|
// If we picked up a block to trim the in point of
|
|
if (trim_in_) {
|
|
trim_in_old_length_ = trim_in_->length();
|
|
trim_in_new_length_ = trim_in_->out() - out_;
|
|
}
|
|
|
|
// If we picked up a block to trim the out point of
|
|
if (trim_out_) {
|
|
trim_out_old_length_ = trim_out_->length();
|
|
trim_out_new_length_ = in_ - trim_out_->in();
|
|
}
|
|
|
|
// If we picked up a block to trim the in point of
|
|
if (trim_in_old_length_ != trim_in_new_length_) {
|
|
trim_in_->set_length_and_media_in(trim_in_new_length_);
|
|
}
|
|
|
|
// Remove all blocks that are flagged for removal
|
|
foreach (Block* remove_block, removed_blocks_) {
|
|
track_->RippleRemoveBlock(remove_block);
|
|
|
|
BlockUnlinkAllCommand* unlink_command = new BlockUnlinkAllCommand(remove_block);
|
|
unlink_command->redo();
|
|
remove_block_commands_.append(unlink_command);
|
|
|
|
NodeRemoveWithExclusiveDeps* remove_command = new NodeRemoveWithExclusiveDeps(static_cast<NodeGraph*>(remove_block->parent()), remove_block);
|
|
remove_command->redo();
|
|
remove_block_commands_.append(remove_command);
|
|
}
|
|
|
|
// If we picked up a block to trim the out point of
|
|
if (trim_out_old_length_ != trim_out_new_length_) {
|
|
trim_out_->set_length_and_media_out(trim_out_new_length_);
|
|
}
|
|
|
|
}
|
|
|
|
// If we were given a block to insert, insert it here
|
|
if (insert_) {
|
|
if (!trim_out_) {
|
|
// This is the start of the Sequence
|
|
track_->PrependBlock(insert_);
|
|
} else if (!trim_in_) {
|
|
// This is the end of the Sequence
|
|
track_->AppendBlock(insert_);
|
|
} else {
|
|
// This is somewhere in the middle of the Sequence
|
|
track_->InsertBlockAfter(insert_, trim_out_);
|
|
}
|
|
}
|
|
|
|
track_->EndOperation();
|
|
|
|
track_->InvalidateCache(TimeRange(in_, insert_ ? out_ : RATIONAL_MAX),
|
|
track_->block_input(),
|
|
track_->block_input());
|
|
}
|
|
|
|
void TrackRippleRemoveAreaCommand::undo_internal()
|
|
{
|
|
track_->BeginOperation();
|
|
|
|
// If we were given a block to insert, insert it here
|
|
if (insert_ != nullptr) {
|
|
track_->RippleRemoveBlock(insert_);
|
|
}
|
|
|
|
if (splice_) {
|
|
|
|
// trim_in_ is our copy and trim_out_ is our original
|
|
track_->RippleRemoveBlock(trim_in_);
|
|
trim_out_->set_length_and_media_out(trim_out_old_length_);
|
|
|
|
delete TakeNodeFromParentGraph(trim_in_);
|
|
|
|
} else {
|
|
|
|
// If we picked up a block to trim the out point of
|
|
if (trim_out_old_length_ != trim_out_new_length_) {
|
|
trim_out_->set_length_and_media_out(trim_out_old_length_);
|
|
}
|
|
|
|
// Restore blocks that were removed
|
|
for (int i=remove_block_commands_.size()-1;i>=0;i--) {
|
|
UndoCommand* command = remove_block_commands_.at(i);
|
|
command->undo();
|
|
delete command;
|
|
}
|
|
remove_block_commands_.clear();
|
|
|
|
for (int i=removed_blocks_.size()-1;i>=0;i--) {
|
|
Block* remove_block = removed_blocks_.at(i);
|
|
|
|
if (trim_in_) {
|
|
track_->InsertBlockBefore(remove_block, trim_in_);
|
|
} else {
|
|
track_->AppendBlock(remove_block);
|
|
}
|
|
}
|
|
removed_blocks_.clear();
|
|
|
|
// If we picked up a block to trim the in point of
|
|
if (trim_in_old_length_ != trim_in_new_length_) {
|
|
trim_in_->set_length_and_media_in(trim_in_old_length_);
|
|
}
|
|
|
|
}
|
|
|
|
track_->EndOperation();
|
|
|
|
track_->InvalidateCache(TimeRange(in_, insert_ ? out_ : RATIONAL_MAX), track_->block_input(), track_->block_input());
|
|
}
|
|
|
|
TrackPlaceBlockCommand::TrackPlaceBlockCommand(TrackList *timeline, int track, Block *block, rational in, QUndoCommand *parent) :
|
|
TrackRippleRemoveAreaCommand(nullptr, in, 0, parent), // Out gets set correctly in redo()
|
|
timeline_(timeline),
|
|
track_index_(track),
|
|
gap_(nullptr)
|
|
{
|
|
insert_ = block;
|
|
}
|
|
|
|
Project *TrackPlaceBlockCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(static_cast<ViewerOutput*>(timeline_->parent())->parent())->project();
|
|
}
|
|
|
|
void TrackPlaceBlockCommand::redo_internal()
|
|
{
|
|
added_track_count_ = 0;
|
|
|
|
// Get track (or make it if necessary)
|
|
while (track_index_ >= timeline_->GetTracks().size()) {
|
|
timeline_->AddTrack();
|
|
|
|
added_track_count_++;
|
|
}
|
|
|
|
track_ = timeline_->GetTrackAt(track_index_);
|
|
|
|
append_ = (in_ >= track_->track_length());
|
|
|
|
// Check if the placement location is past the end of the timeline
|
|
if (append_) {
|
|
if (in_ > track_->track_length()) {
|
|
// If so, insert a gap here
|
|
gap_ = new GapBlock();
|
|
gap_->set_length_and_media_out(in_ - track_->track_length());
|
|
static_cast<NodeGraph*>(track_->parent())->AddNode(gap_);
|
|
track_->AppendBlock(gap_);
|
|
}
|
|
|
|
track_->AppendBlock(insert_);
|
|
} else {
|
|
out_ = in_ + insert_->length();
|
|
|
|
// Place the Block at this point
|
|
TrackRippleRemoveAreaCommand::redo_internal();
|
|
}
|
|
}
|
|
|
|
void TrackPlaceBlockCommand::undo_internal()
|
|
{
|
|
if (append_) {
|
|
track_->RippleRemoveBlock(insert_);
|
|
|
|
if (gap_ != nullptr) {
|
|
track_->RippleRemoveBlock(gap_);
|
|
delete TakeNodeFromParentGraph(gap_);
|
|
}
|
|
} else {
|
|
TrackRippleRemoveAreaCommand::undo_internal();
|
|
}
|
|
|
|
for (;added_track_count_>0;added_track_count_--) {
|
|
timeline_->RemoveTrack();
|
|
}
|
|
}
|
|
|
|
BlockSplitCommand::BlockSplitCommand(TrackOutput* track, Block *block, rational point, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
track_(track),
|
|
block_(block),
|
|
new_length_(point - block->in()),
|
|
old_length_(block->length()),
|
|
point_(point)
|
|
{
|
|
Q_ASSERT(point > block_->in() && point < block_->out() && block_->type() == Block::kClip);
|
|
|
|
// Ensures that this block is deleted if this action is undone
|
|
new_block_ = static_cast<Block*>(block_->copy());
|
|
new_block_->setParent(&memory_manager_);
|
|
|
|
// Determine if the block outputs to an "out" transition
|
|
foreach (NodeEdgePtr edge, block_->output()->edges()) {
|
|
if (edge->input()->parentNode()->IsBlock()
|
|
&& static_cast<Block*>(edge->input()->parentNode())->type() == Block::kTransition
|
|
&& edge->input() == static_cast<TransitionBlock*>(edge->input()->parentNode())->out_block_input()) {
|
|
transitions_to_move_.append(edge->input());
|
|
}
|
|
}
|
|
}
|
|
|
|
Project *BlockSplitCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void BlockSplitCommand::redo_internal()
|
|
{
|
|
track_->BeginOperation();
|
|
|
|
static_cast<NodeGraph*>(block_->parent())->AddNode(new_block_);
|
|
Node::CopyInputs(block_, new_block_);
|
|
|
|
rational new_part_length = block_->length() - (point_ - block_->in());
|
|
|
|
block_->set_length_and_media_out(new_length_);
|
|
|
|
new_block_->set_length_and_media_in(new_part_length);
|
|
|
|
track_->InsertBlockAfter(new_block_, block_);
|
|
|
|
foreach (NodeInput* transition, transitions_to_move_) {
|
|
NodeParam::DisconnectEdge(block_->output(), transition);
|
|
NodeParam::ConnectEdge(new_block_->output(), transition);
|
|
}
|
|
|
|
track_->EndOperation();
|
|
}
|
|
|
|
void BlockSplitCommand::undo_internal()
|
|
{
|
|
track_->BeginOperation();
|
|
|
|
block_->set_length_and_media_out(old_length_);
|
|
track_->RippleRemoveBlock(new_block_);
|
|
|
|
TakeNodeFromParentGraph(new_block_, &memory_manager_);
|
|
|
|
foreach (NodeInput* transition, transitions_to_move_) {
|
|
NodeParam::DisconnectEdge(new_block_->output(), transition);
|
|
NodeParam::ConnectEdge(block_->output(), transition);
|
|
}
|
|
|
|
track_->EndOperation();
|
|
}
|
|
|
|
Block *BlockSplitCommand::new_block()
|
|
{
|
|
return new_block_;
|
|
}
|
|
|
|
TrackSplitAtTimeCommand::TrackSplitAtTimeCommand(TrackOutput *track, rational point, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
track_(track)
|
|
{
|
|
// Find Block that contains this time
|
|
foreach (Block* b, track->Blocks()) {
|
|
if (b->out() == point) {
|
|
// This time is between blocks, no split needs to occur
|
|
return;
|
|
} else if (b->in() < point && b->out() > point) {
|
|
// We found the Block, split it
|
|
new BlockSplitCommand(track_, b, point, this);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
Project *TrackSplitAtTimeCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(track_->parent())->project();
|
|
}
|
|
|
|
TrackReplaceBlockCommand::TrackReplaceBlockCommand(TrackOutput* track, Block *old, Block *replace, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
track_(track),
|
|
old_(old),
|
|
replace_(replace)
|
|
{
|
|
}
|
|
|
|
Project *TrackReplaceBlockCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(track_->parent())->project();
|
|
}
|
|
|
|
void TrackReplaceBlockCommand::redo_internal()
|
|
{
|
|
track_->ReplaceBlock(old_, replace_);
|
|
}
|
|
|
|
void TrackReplaceBlockCommand::undo_internal()
|
|
{
|
|
track_->ReplaceBlock(replace_, old_);
|
|
}
|
|
|
|
TrackPrependBlockCommand::TrackPrependBlockCommand(TrackOutput *track, Block *block, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
track_(track),
|
|
block_(block)
|
|
{
|
|
}
|
|
|
|
Project *TrackPrependBlockCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(track_->parent())->project();
|
|
}
|
|
|
|
void TrackPrependBlockCommand::redo_internal()
|
|
{
|
|
track_->PrependBlock(block_);
|
|
}
|
|
|
|
void TrackPrependBlockCommand::undo_internal()
|
|
{
|
|
track_->RippleRemoveBlock(block_);
|
|
}
|
|
|
|
BlockSplitPreservingLinksCommand::BlockSplitPreservingLinksCommand(const QVector<Block *> &blocks, const QList<rational> ×, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
blocks_(blocks),
|
|
times_(times)
|
|
{
|
|
QVector< QVector<Block*> > split_blocks(times.size());
|
|
|
|
for (int i=0;i<times.size();i++) {
|
|
const rational& time = times.at(i);
|
|
|
|
QVector<Block*> splits(blocks.size());
|
|
|
|
for (int j=0;j<blocks.size();j++) {
|
|
Block* b = blocks.at(j);
|
|
|
|
if (b->in() < time && b->out() > time) {
|
|
TrackOutput* track = TrackOutput::TrackFromBlock(b);
|
|
|
|
Q_ASSERT(track);
|
|
|
|
BlockSplitCommand* split_command = new BlockSplitCommand(track, b, time, this);
|
|
splits.replace(j, split_command->new_block());
|
|
} else {
|
|
splits.replace(j, nullptr);
|
|
}
|
|
}
|
|
|
|
split_blocks.replace(i, splits);
|
|
}
|
|
|
|
// Now that we've determined all the splits, we can relink everything
|
|
for (int i=0;i<blocks_.size();i++) {
|
|
Block* a = blocks.at(i);
|
|
|
|
for (int j=0;j<blocks.size();j++) {
|
|
if (i == j) {
|
|
continue;
|
|
}
|
|
|
|
Block* b = blocks.at(j);
|
|
|
|
if (Block::AreLinked(a, b)) {
|
|
// These blocks are linked, ensure all the splits are linked too
|
|
|
|
foreach (const QVector<Block*>& split_list, split_blocks) {
|
|
Block::Link(split_list.at(i), split_list.at(j));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Project *BlockSplitPreservingLinksCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(blocks_.first()->parent())->project();
|
|
}
|
|
|
|
BlockSetSpeedCommand::BlockSetSpeedCommand(Block *block, const rational &new_speed, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
block_(block),
|
|
old_speed_(block->speed()),
|
|
new_speed_(new_speed)
|
|
{
|
|
}
|
|
|
|
Project *BlockSetSpeedCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void BlockSetSpeedCommand::redo_internal()
|
|
{
|
|
block_->set_speed(new_speed_);
|
|
}
|
|
|
|
void BlockSetSpeedCommand::undo_internal()
|
|
{
|
|
block_->set_speed(old_speed_);
|
|
}
|
|
|
|
TimelineRippleDeleteGapsAtRegionsCommand::TimelineRippleDeleteGapsAtRegionsCommand(ViewerOutput *vo, const TimeRangeList ®ions, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
timeline_(vo),
|
|
regions_(regions)
|
|
{
|
|
}
|
|
|
|
Project *TimelineRippleDeleteGapsAtRegionsCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(timeline_->parent())->project();
|
|
}
|
|
|
|
void TimelineRippleDeleteGapsAtRegionsCommand::redo_internal()
|
|
{
|
|
foreach (const TimeRange& range, regions_) {
|
|
rational max_ripple_length = range.length();
|
|
|
|
QList<Block*> blocks_around_range;
|
|
|
|
foreach (TrackOutput* track, timeline_->GetTracks()) {
|
|
// Get the block from every other track that is either at or just before our block's in point
|
|
Block* block_at_time = track->NearestBlockBeforeOrAt(range.in());
|
|
|
|
if (block_at_time) {
|
|
if (block_at_time->type() == Block::kGap) {
|
|
max_ripple_length = qMin(block_at_time->length(), max_ripple_length);
|
|
} else {
|
|
max_ripple_length = 0;
|
|
break;
|
|
}
|
|
|
|
blocks_around_range.append(block_at_time);
|
|
}
|
|
}
|
|
|
|
if (max_ripple_length > 0) {
|
|
foreach (Block* resize, blocks_around_range) {
|
|
if (resize->length() == max_ripple_length) {
|
|
// Remove block entirely
|
|
TrackRippleRemoveBlockCommand* remove_command = new TrackRippleRemoveBlockCommand(TrackOutput::TrackFromBlock(resize), resize);
|
|
remove_command->redo();
|
|
commands_.append(remove_command);
|
|
} else {
|
|
// Resize block
|
|
BlockResizeCommand* resize_command = new BlockResizeCommand(resize, resize->length() - max_ripple_length);
|
|
resize_command->redo();
|
|
commands_.append(resize_command);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TimelineRippleDeleteGapsAtRegionsCommand::undo_internal()
|
|
{
|
|
for (int i=commands_.size()-1;i>=0;i--) {
|
|
commands_.at(i)->undo();
|
|
delete commands_.at(i);
|
|
}
|
|
commands_.empty();
|
|
}
|
|
|
|
WorkareaSetEnabledCommand::WorkareaSetEnabledCommand(Project* project, TimelinePoints *points, bool enabled, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
project_(project),
|
|
points_(points),
|
|
old_enabled_(points_->workarea()->enabled()),
|
|
new_enabled_(enabled)
|
|
{
|
|
}
|
|
|
|
Project *WorkareaSetEnabledCommand::GetRelevantProject() const
|
|
{
|
|
return project_;
|
|
}
|
|
|
|
void WorkareaSetEnabledCommand::redo_internal()
|
|
{
|
|
points_->workarea()->set_enabled(new_enabled_);
|
|
}
|
|
|
|
void WorkareaSetEnabledCommand::undo_internal()
|
|
{
|
|
points_->workarea()->set_enabled(old_enabled_);
|
|
}
|
|
|
|
WorkareaSetRangeCommand::WorkareaSetRangeCommand(Project* project, TimelinePoints *points, const TimeRange &range, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
project_(project),
|
|
points_(points),
|
|
old_range_(points_->workarea()->range()),
|
|
new_range_(range)
|
|
{
|
|
}
|
|
|
|
Project *WorkareaSetRangeCommand::GetRelevantProject() const
|
|
{
|
|
return project_;
|
|
}
|
|
|
|
void WorkareaSetRangeCommand::redo_internal()
|
|
{
|
|
points_->workarea()->set_range(new_range_);
|
|
}
|
|
|
|
void WorkareaSetRangeCommand::undo_internal()
|
|
{
|
|
points_->workarea()->set_range(old_range_);
|
|
}
|
|
|
|
BlockLinkCommand::BlockLinkCommand(Block *a, Block *b, bool link, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
a_(a),
|
|
b_(b),
|
|
link_(link)
|
|
{
|
|
}
|
|
|
|
Project *BlockLinkCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(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<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void BlockUnlinkAllCommand::redo_internal()
|
|
{
|
|
unlinked_ = block_->linked_clips();
|
|
|
|
foreach (Block* link, unlinked_) {
|
|
Block::Unlink(block_, link);
|
|
}
|
|
}
|
|
|
|
void BlockUnlinkAllCommand::undo_internal()
|
|
{
|
|
foreach (Block* link, unlinked_) {
|
|
Block::Link(block_, link);
|
|
}
|
|
|
|
unlinked_.clear();
|
|
}
|
|
|
|
BlockLinkManyCommand::BlockLinkManyCommand(const QList<Block *> 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<Sequence*>(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<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void BlockEnableDisableCommand::redo_internal()
|
|
{
|
|
block_->set_enabled(new_enabled_);
|
|
}
|
|
|
|
void BlockEnableDisableCommand::undo_internal()
|
|
{
|
|
block_->set_enabled(old_enabled_);
|
|
}
|
|
|
|
BlockTrimCommand::BlockTrimCommand(TrackOutput* track, Block *block, rational new_length, Timeline::MovementMode mode, QUndoCommand *command) :
|
|
UndoCommand(command),
|
|
track_(track),
|
|
block_(block),
|
|
old_length_(block->length()),
|
|
new_length_(new_length),
|
|
mode_(mode),
|
|
adjacent_(nullptr),
|
|
we_created_adjacent_(false),
|
|
we_deleted_adjacent_(false),
|
|
allow_nongap_trimming_(false)
|
|
{
|
|
}
|
|
|
|
Project *BlockTrimCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(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 || allow_nongap_trimming_)) {
|
|
|
|
// A gap (or equivalent) exists, simply increase the size of it
|
|
if (mode_ == Timeline::kTrimIn) {
|
|
adjacent_->set_length_and_media_out(adjacent_->length() + trim_diff);
|
|
} else {
|
|
adjacent_->set_length_and_media_in(adjacent_->length() + trim_diff);
|
|
}
|
|
|
|
} else {
|
|
|
|
// Don't create a gap if the trim was at the end of the sequence (which would be indicated by
|
|
// the mode being "trim out" and "block_->next()" being null.
|
|
if (mode_ == Timeline::kTrimIn || block_->next()) {
|
|
// We must create a gap
|
|
we_created_adjacent_ = true;
|
|
|
|
adjacent_ = new GapBlock();
|
|
adjacent_->set_length_and_media_out(trim_diff);
|
|
static_cast<NodeGraph*>(track_->parent())->AddNode(adjacent_);
|
|
|
|
if (mode_ == Timeline::kTrimIn) {
|
|
track_->InsertBlockBefore(adjacent_, block_);
|
|
} else {
|
|
track_->InsertBlockAfter(adjacent_, block_);
|
|
}
|
|
}
|
|
|
|
}
|
|
} else {
|
|
if (adjacent_) {
|
|
// If trimming LONGER, we'll need to trim the adjacent
|
|
// (assume if there's no adjacent, we're at the end of the timeline and do nothing)
|
|
rational adjacent_length = adjacent_->length() + trim_diff;
|
|
|
|
if (adjacent_length.isNull()) {
|
|
// Ripple remove block
|
|
track_->RippleRemoveBlock(adjacent_);
|
|
TakeNodeFromParentGraph(adjacent_, &memory_manager_);
|
|
we_deleted_adjacent_ = true;
|
|
} else if (mode_ == Timeline::kTrimIn) {
|
|
adjacent_->set_length_and_media_out(adjacent_length);
|
|
} else {
|
|
adjacent_->set_length_and_media_in(adjacent_length);
|
|
}
|
|
}
|
|
}
|
|
|
|
track_->EndOperation();
|
|
|
|
track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input());
|
|
}
|
|
|
|
void BlockTrimCommand::undo_internal()
|
|
{
|
|
track_->BeginOperation();
|
|
|
|
// Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer
|
|
rational trim_diff = old_length_ - new_length_;
|
|
|
|
if (trim_diff > rational()) {
|
|
// If trimmed SHORTER, we need to unadjust the gap
|
|
if (we_created_adjacent_) {
|
|
// If we created a gap, just remove it straight up
|
|
track_->RippleRemoveBlock(adjacent_);
|
|
delete TakeNodeFromParentGraph(adjacent_);
|
|
adjacent_ = nullptr;
|
|
we_created_adjacent_ = false;
|
|
} else if (adjacent_) {
|
|
// If we adjusted an existing gap, unadjust here
|
|
adjacent_->set_length_and_media_out(adjacent_->length() - trim_diff);
|
|
}
|
|
} else {
|
|
if (adjacent_) {
|
|
// If trimmed LONGER, we adjusted an existing block
|
|
// (assume if there's no adjacent, we're at the end of the timeline and do nothing)
|
|
if (we_deleted_adjacent_) {
|
|
static_cast<NodeGraph*>(track_->parent())->AddNode(adjacent_);
|
|
|
|
if (mode_ == Timeline::kTrimIn) {
|
|
track_->InsertBlockBefore(adjacent_, block_);
|
|
} else {
|
|
track_->InsertBlockAfter(adjacent_, block_);
|
|
}
|
|
|
|
we_deleted_adjacent_ = false;
|
|
} else if (mode_ == Timeline::kTrimIn) {
|
|
adjacent_->set_length_and_media_out(adjacent_->length() - trim_diff);
|
|
} else {
|
|
adjacent_->set_length_and_media_in(adjacent_->length() - trim_diff);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
track_->EndOperation();
|
|
|
|
track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input());
|
|
}
|
|
|
|
TrackReplaceBlockWithGapCommand::TrackReplaceBlockWithGapCommand(TrackOutput *track, Block *block, QUndoCommand *command) :
|
|
UndoCommand(command),
|
|
track_(track),
|
|
block_(block),
|
|
we_created_gap_(false),
|
|
gap_(nullptr),
|
|
merged_gap_(nullptr)
|
|
{
|
|
}
|
|
|
|
Project *TrackReplaceBlockWithGapCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(block_->parent())->project();
|
|
}
|
|
|
|
void TrackReplaceBlockWithGapCommand::redo_internal()
|
|
{
|
|
TimeRange invalidate_range;
|
|
|
|
track_->BeginOperation();
|
|
|
|
// If the block has no next, it's at the end of the track and there's no need to create a gap
|
|
if (block_->next()) {
|
|
invalidate_range = TimeRange(block_->in(), block_->out());
|
|
|
|
rational new_gap_length = block_->length();
|
|
|
|
bool previous_is_a_gap = (block_->previous() && block_->previous()->type() == Block::kGap);
|
|
bool next_is_a_gap = (block_->next() && block_->next()->type() == Block::kGap);
|
|
|
|
if (previous_is_a_gap) {
|
|
// Extend gap before this block
|
|
gap_ = static_cast<GapBlock*>(block_->previous());
|
|
|
|
// If the next is also a gap, we'll merge the two
|
|
if (next_is_a_gap) {
|
|
merged_gap_ = static_cast<GapBlock*>(block_->next());
|
|
|
|
new_gap_length += merged_gap_->length();
|
|
track_->RippleRemoveBlock(merged_gap_);
|
|
TakeNodeFromParentGraph(merged_gap_, &memory_manager_);
|
|
}
|
|
} else if (next_is_a_gap) {
|
|
// Extend gap after this block
|
|
gap_ = static_cast<GapBlock*>(block_->next());
|
|
}
|
|
|
|
if (gap_) {
|
|
// Extend an existing gap
|
|
new_gap_length += gap_->length();
|
|
gap_->set_length_and_media_out(new_gap_length);
|
|
track_->RippleRemoveBlock(block_);
|
|
} else {
|
|
// No gap exists, create one
|
|
gap_ = new GapBlock();
|
|
gap_->set_length_and_media_out(new_gap_length);
|
|
static_cast<NodeGraph*>(track_->parent())->AddNode(gap_);
|
|
track_->ReplaceBlock(block_, gap_);
|
|
we_created_gap_ = true;
|
|
}
|
|
|
|
} else {
|
|
rational earliest_change = block_->in();
|
|
|
|
// Handle the gap being at the end where no gap is necessary
|
|
track_->RippleRemoveBlock(block_);
|
|
|
|
// If there were also gaps leading up to this block, clean them up here
|
|
if (!track_->Blocks().isEmpty() && track_->Blocks().last()->type() == Block::kGap) {
|
|
merged_gap_ = static_cast<GapBlock*>(track_->Blocks().last());
|
|
earliest_change = merged_gap_->in();
|
|
track_->RippleRemoveBlock(merged_gap_);
|
|
TakeNodeFromParentGraph(merged_gap_, &memory_manager_);
|
|
}
|
|
|
|
invalidate_range = TimeRange(earliest_change, RATIONAL_MAX);
|
|
}
|
|
|
|
track_->EndOperation();
|
|
|
|
track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input());
|
|
}
|
|
|
|
void TrackReplaceBlockWithGapCommand::undo_internal()
|
|
{
|
|
TimeRange invalidate_range;
|
|
|
|
track_->BeginOperation();
|
|
|
|
if (gap_) {
|
|
|
|
if (we_created_gap_) {
|
|
// We made this gap, simply swap our gap back
|
|
track_->ReplaceBlock(gap_, block_);
|
|
delete TakeNodeFromParentGraph(gap_);
|
|
gap_ = nullptr;
|
|
} else {
|
|
// We must have extended an existing gap
|
|
rational original_gap_length = gap_->length() - block_->length();
|
|
|
|
// If we merged two gaps together, restore it now
|
|
if (merged_gap_) {
|
|
original_gap_length -= merged_gap_->length();
|
|
static_cast<NodeGraph*>(track_->parent())->AddNode(merged_gap_);
|
|
track_->InsertBlockAfter(merged_gap_, gap_);
|
|
}
|
|
|
|
// Restore original block
|
|
track_->InsertBlockAfter(block_, gap_);
|
|
|
|
// Restore gap's original length
|
|
gap_->set_length_and_media_out(original_gap_length);
|
|
}
|
|
|
|
invalidate_range = TimeRange(block_->in(), block_->out());
|
|
|
|
} else {
|
|
|
|
// If there's no `gap_`, we must have removed the block at the end
|
|
invalidate_range = TimeRange(track_->track_length(), RATIONAL_MAX);
|
|
|
|
if (merged_gap_) {
|
|
static_cast<NodeGraph*>(track_->parent())->AddNode(merged_gap_);
|
|
track_->AppendBlock(merged_gap_);
|
|
}
|
|
|
|
track_->AppendBlock(block_);
|
|
|
|
}
|
|
|
|
merged_gap_ = nullptr;
|
|
|
|
track_->EndOperation();
|
|
|
|
track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input());
|
|
}
|
|
|
|
TrackSlideCommand::TrackSlideCommand(const QVector<TrackSlideCommand::BlockSlideInfo> &blocks, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
blocks_(blocks)
|
|
{
|
|
}
|
|
|
|
Project *TrackSlideCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(blocks_.first().track->parent())->project();
|
|
}
|
|
|
|
void TrackSlideCommand::redo_internal()
|
|
{
|
|
slide_internal(false);
|
|
}
|
|
|
|
void TrackSlideCommand::undo_internal()
|
|
{
|
|
slide_internal(true);
|
|
}
|
|
|
|
void TrackSlideCommand::slide_internal(bool undo)
|
|
{
|
|
QMap<TrackOutput*, TimeRangeList> invalidate_ranges;
|
|
|
|
// Make sure all movement blocks' old positions are invalidated
|
|
foreach (const BlockSlideInfo& info, blocks_) {
|
|
if (info.mode == Timeline::kMove) {
|
|
invalidate_ranges[info.track].InsertTimeRange(TimeRange(info.block->in(),
|
|
info.block->out()));
|
|
}
|
|
}
|
|
|
|
// Perform trims
|
|
foreach (const BlockSlideInfo& info, blocks_) {
|
|
info.track->BeginOperation();
|
|
|
|
if (info.mode == Timeline::kTrimIn || info.mode == Timeline::kTrimOut) {
|
|
rational new_len = undo ? info.old_time : info.new_time;
|
|
|
|
if (info.mode == Timeline::kTrimIn) {
|
|
info.block->set_length_and_media_in(new_len);
|
|
} else {
|
|
info.block->set_length_and_media_out(new_len);
|
|
}
|
|
} else if (!undo && info.mode == Timeline::kMove && !info.block->previous()) {
|
|
// If this is a moving block and there was nothing before it to offset its time correctly,
|
|
// insert a gap here
|
|
GapBlock* gap = new GapBlock();
|
|
gap->set_length_and_media_out(info.new_time);
|
|
static_cast<NodeGraph*>(info.block->parent())->AddNode(gap);
|
|
info.track->PrependBlock(gap);
|
|
added_gaps_.append(gap);
|
|
}
|
|
|
|
info.track->EndOperation();
|
|
}
|
|
|
|
if (undo) {
|
|
// If undoing, remove added gaps
|
|
foreach (GapBlock* gap, added_gaps_) {
|
|
TrackOutput* track = TrackOutput::TrackFromBlock(gap);
|
|
|
|
track->BeginOperation();
|
|
|
|
track->RippleRemoveBlock(gap);
|
|
delete TakeNodeFromParentGraph(gap);
|
|
|
|
track->EndOperation();
|
|
}
|
|
|
|
added_gaps_.clear();
|
|
}
|
|
|
|
// Make sure all movement blocks' new positions are invalidated
|
|
foreach (const BlockSlideInfo& info, blocks_) {
|
|
if (info.mode == Timeline::kMove) {
|
|
invalidate_ranges[info.track].InsertTimeRange(TimeRange(info.block->in(),
|
|
info.block->out()));
|
|
}
|
|
}
|
|
|
|
QMap<TrackOutput*, TimeRangeList>::const_iterator i;
|
|
for (i=invalidate_ranges.constBegin(); i!=invalidate_ranges.constEnd(); i++) {
|
|
foreach (const TimeRange& r, i.value()) {
|
|
i.key()->InvalidateCache(r, i.key()->block_input(), i.key()->block_input());
|
|
}
|
|
}
|
|
}
|
|
|
|
TrackListRippleRemoveAreaCommand::TrackListRippleRemoveAreaCommand(TrackList *list, rational in, rational out, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
list_(list),
|
|
in_(in),
|
|
out_(out)
|
|
{
|
|
all_tracks_unlocked_ = true;
|
|
|
|
foreach (TrackOutput* track, list_->GetTracks()) {
|
|
if (track->IsLocked()) {
|
|
all_tracks_unlocked_ = false;
|
|
continue;
|
|
}
|
|
|
|
TrackRippleRemoveAreaCommand* c = new TrackRippleRemoveAreaCommand(track, in, out);
|
|
commands_.append(c);
|
|
working_tracks_.append(track);
|
|
}
|
|
}
|
|
|
|
TrackListRippleRemoveAreaCommand::~TrackListRippleRemoveAreaCommand()
|
|
{
|
|
qDeleteAll(commands_);
|
|
}
|
|
|
|
Project *TrackListRippleRemoveAreaCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(static_cast<ViewerOutput*>(list_->parent())->parent())->project();
|
|
}
|
|
|
|
void TrackListRippleRemoveAreaCommand::redo_internal()
|
|
{
|
|
if (all_tracks_unlocked_) {
|
|
// We can optimize here by simply shifting the whole cache forward instead of re-caching
|
|
// everything following this time
|
|
if (list_->type() == Timeline::kTrackTypeVideo) {
|
|
static_cast<ViewerOutput*>(list_->parent())->ShiftVideoCache(out_, in_);
|
|
} else if (list_->type() == Timeline::kTrackTypeAudio) {
|
|
static_cast<ViewerOutput*>(list_->parent())->ShiftAudioCache(out_, in_);
|
|
}
|
|
|
|
foreach (TrackOutput* track, working_tracks_) {
|
|
track->BeginOperation();
|
|
}
|
|
}
|
|
|
|
foreach (TrackRippleRemoveAreaCommand* c, commands_) {
|
|
c->redo();
|
|
}
|
|
|
|
if (all_tracks_unlocked_) {
|
|
foreach (TrackOutput* track, working_tracks_) {
|
|
track->EndOperation();
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrackListRippleRemoveAreaCommand::undo_internal()
|
|
{
|
|
if (all_tracks_unlocked_) {
|
|
// We can optimize here by simply shifting the whole cache forward instead of re-caching
|
|
// everything following this time
|
|
if (list_->type() == Timeline::kTrackTypeVideo) {
|
|
static_cast<ViewerOutput*>(list_->parent())->ShiftVideoCache(in_, out_);
|
|
} else if (list_->type() == Timeline::kTrackTypeAudio) {
|
|
static_cast<ViewerOutput*>(list_->parent())->ShiftAudioCache(in_, out_);
|
|
}
|
|
|
|
foreach (TrackOutput* track, working_tracks_) {
|
|
track->BeginOperation();
|
|
}
|
|
}
|
|
|
|
foreach (TrackRippleRemoveAreaCommand* c, commands_) {
|
|
c->undo();
|
|
}
|
|
|
|
if (all_tracks_unlocked_) {
|
|
foreach (TrackOutput* track, working_tracks_) {
|
|
track->EndOperation();
|
|
}
|
|
}
|
|
}
|
|
|
|
TimelineRippleRemoveAreaCommand::TimelineRippleRemoveAreaCommand(ViewerOutput *timeline, rational in, rational out, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
timeline_(timeline)
|
|
{
|
|
for (int i=0; i<Timeline::kTrackTypeCount; i++) {
|
|
new TrackListRippleRemoveAreaCommand(timeline->track_list(static_cast<Timeline::TrackType>(i)),
|
|
in,
|
|
out,
|
|
this);
|
|
}
|
|
}
|
|
|
|
Project *TimelineRippleRemoveAreaCommand::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(timeline_->parent())->project();
|
|
}
|
|
|
|
TrackListRippleToolCommand::TrackListRippleToolCommand(TrackList *track_list, const QList<RippleInfo> &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<Sequence*>(static_cast<ViewerOutput*>(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<info_.size();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.new_length);
|
|
} else {
|
|
b->set_length_and_media_out(info.new_length);
|
|
}
|
|
} else {
|
|
// Assume the Block was a Gap and it was reduced to zero length, remove it here
|
|
working_data_[i].removed_gap_after = b->previous();
|
|
info.track->RippleRemoveBlock(b);
|
|
TakeNodeFromParentGraph(b, &memory_manager_);
|
|
}
|
|
} else if (info.new_length > 0) {
|
|
// This is a gap we are creating
|
|
GapBlock* gap = new GapBlock();
|
|
gap->set_length_and_media_out(info.new_length);
|
|
static_cast<NodeGraph*>(info.ref_block->parent())->AddNode(gap);
|
|
working_data_[i].created_gap = gap;
|
|
|
|
info.track->InsertBlockAfter(gap, info.ref_block);
|
|
}
|
|
}
|
|
|
|
if (all_tracks_unlocked_) {
|
|
// We can do some optimization here
|
|
|
|
rational new_latest_pt = RATIONAL_MIN;
|
|
for (int i=0;i<info_.size();i++) {
|
|
const RippleInfo& info = info_.at(i);
|
|
|
|
if (info.block) {
|
|
if (info.new_length > 0) {
|
|
new_latest_pt = qMax(new_latest_pt, info.block->out());
|
|
} else {
|
|
new_latest_pt = qMax(new_latest_pt, info.block->in());
|
|
}
|
|
} else if (info.new_length > 0) {
|
|
new_latest_pt = qMax(new_latest_pt, working_data_.at(i).created_gap->out());
|
|
}
|
|
}
|
|
|
|
if (track_list_->type() == Timeline::kTrackTypeVideo) {
|
|
static_cast<ViewerOutput*>(track_list_->parent())->ShiftVideoCache(old_latest_pt, new_latest_pt);
|
|
} else if (track_list_->type() == Timeline::kTrackTypeAudio) {
|
|
static_cast<ViewerOutput*>(track_list_->parent())->ShiftAudioCache(old_latest_pt, new_latest_pt);
|
|
}
|
|
|
|
foreach (const RippleInfo& info, info_) {
|
|
info.track->EndOperation();
|
|
|
|
// FIXME: Untested, is this desirable behavior?
|
|
if (earliest_pt < new_latest_pt) {
|
|
info.track->InvalidateCache(TimeRange(earliest_pt, new_latest_pt),
|
|
info.track->block_input(),
|
|
info.track->block_input());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrackListRippleToolCommand::undo_internal()
|
|
{
|
|
// FIXME: Add cache shift optimization
|
|
|
|
// Clean created gaps
|
|
for (int i=info_.size()-1; i>=0; i--) {
|
|
const RippleInfo& info = info_.at(i);
|
|
|
|
Block* b = info.block;
|
|
|
|
if (b) {
|
|
// This was a Block that already existed
|
|
if (info.new_length > 0) {
|
|
if (movement_mode_ == Timeline::kTrimIn) {
|
|
// We'll need to shift the media in point too
|
|
b->set_length_and_media_in(info.old_length);
|
|
} else {
|
|
b->set_length_and_media_out(info.old_length);
|
|
}
|
|
} else {
|
|
// Assume the Block was a Gap and it was reduced to zero length, remove it here
|
|
Block* previous_block = working_data_[i].removed_gap_after;
|
|
|
|
static_cast<NodeGraph*>(info.track->parent())->AddNode(b);
|
|
|
|
if (previous_block) {
|
|
info.track->InsertBlockAfter(b, previous_block);
|
|
} else {
|
|
info.track->PrependBlock(b);
|
|
}
|
|
}
|
|
} else if (info.new_length > 0) {
|
|
// We created a gap here, remove it
|
|
GapBlock* gap = working_data_.at(i).created_gap;
|
|
|
|
info.track->RippleRemoveBlock(gap);
|
|
delete TakeNodeFromParentGraph(gap);
|
|
}
|
|
}
|
|
}
|
|
|
|
TrackListInsertGaps::TrackListInsertGaps(TrackList *track_list, const rational &point, const rational &length, QUndoCommand *parent) :
|
|
UndoCommand(parent),
|
|
track_list_(track_list),
|
|
point_(point),
|
|
length_(length),
|
|
split_command_(nullptr)
|
|
{
|
|
all_tracks_unlocked_ = true;
|
|
|
|
foreach (TrackOutput* track, track_list_->GetTracks()) {
|
|
if (track->IsLocked()) {
|
|
all_tracks_unlocked_ = false;
|
|
continue;
|
|
}
|
|
|
|
working_tracks_.append(track);
|
|
}
|
|
}
|
|
|
|
Project *TrackListInsertGaps::GetRelevantProject() const
|
|
{
|
|
return static_cast<Sequence*>(static_cast<ViewerOutput*>(track_list_->parent())->parent())->project();
|
|
}
|
|
|
|
void TrackListInsertGaps::redo_internal()
|
|
{
|
|
if (all_tracks_unlocked_) {
|
|
// Optimize by shifting over since we have a constant amount of time being inserted
|
|
if (track_list_->type() == Timeline::kTrackTypeVideo) {
|
|
static_cast<ViewerOutput*>(track_list_->parent())->ShiftVideoCache(point_, point_ + length_);
|
|
} else if (track_list_->type() == Timeline::kTrackTypeAudio) {
|
|
static_cast<ViewerOutput*>(track_list_->parent())->ShiftAudioCache(point_, point_ + length_);
|
|
}
|
|
|
|
foreach (TrackOutput* track, working_tracks_) {
|
|
track->BeginOperation();
|
|
}
|
|
}
|
|
|
|
QVector<Block*> blocks_to_split;
|
|
QList<Block*> blocks_to_append_gap_to;
|
|
|
|
foreach (TrackOutput* track, working_tracks_) {
|
|
foreach (Block* b, track->Blocks()) {
|
|
if (b->type() == Block::kGap && b->in() <= point_ && b->out() >= point_) {
|
|
gaps_to_extend_.append(b);
|
|
} else if (b->type() == Block::kClip && b->out() >= point_) {
|
|
if (b->out() > point_) {
|
|
blocks_to_split.append(b);
|
|
}
|
|
|
|
blocks_to_append_gap_to.append(b);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (Block* gap, gaps_to_extend_) {
|
|
gap->set_length_and_media_out(gap->length() + length_);
|
|
}
|
|
|
|
if (!blocks_to_split.isEmpty()) {
|
|
split_command_ = new BlockSplitPreservingLinksCommand(blocks_to_split, {point_});
|
|
split_command_->redo();
|
|
}
|
|
|
|
foreach (Block* block, blocks_to_append_gap_to) {
|
|
GapBlock* gap = new GapBlock();
|
|
gap->set_length_and_media_out(length_);
|
|
static_cast<NodeGraph*>(block->parent())->AddNode(gap);
|
|
TrackOutput::TrackFromBlock(block)->InsertBlockAfter(gap, block);
|
|
gaps_added_.append(gap);
|
|
}
|
|
|
|
if (all_tracks_unlocked_) {
|
|
foreach (TrackOutput* track, working_tracks_) {
|
|
track->EndOperation();
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrackListInsertGaps::undo_internal()
|
|
{
|
|
if (all_tracks_unlocked_) {
|
|
// Optimize by shifting over since we have a constant amount of time being inserted
|
|
if (track_list_->type() == Timeline::kTrackTypeVideo) {
|
|
static_cast<ViewerOutput*>(track_list_->parent())->ShiftVideoCache(point_ + length_, point_);
|
|
} else if (track_list_->type() == Timeline::kTrackTypeAudio) {
|
|
static_cast<ViewerOutput*>(track_list_->parent())->ShiftAudioCache(point_ + length_, point_);
|
|
}
|
|
|
|
foreach (TrackOutput* track, working_tracks_) {
|
|
track->BeginOperation();
|
|
}
|
|
}
|
|
|
|
// Remove added gaps
|
|
foreach (GapBlock* gap, gaps_added_) {
|
|
TrackOutput::TrackFromBlock(gap)->RippleRemoveBlock(gap);
|
|
delete TakeNodeFromParentGraph(gap);
|
|
}
|
|
gaps_added_.clear();
|
|
|
|
// Un-split blocks
|
|
if (split_command_) {
|
|
split_command_->undo();
|
|
delete split_command_;
|
|
split_command_ = nullptr;
|
|
}
|
|
|
|
// Restore original length of gaps
|
|
foreach (Block* gap, gaps_to_extend_) {
|
|
gap->set_length_and_media_out(gap->length() - length_);
|
|
}
|
|
gaps_to_extend_.clear();
|
|
|
|
if (all_tracks_unlocked_) {
|
|
foreach (TrackOutput* track, working_tracks_) {
|
|
track->EndOperation();
|
|
}
|
|
}
|
|
}
|
|
|
|
OLIVE_NAMESPACE_EXIT
|