/***
Olive - Non-Linear Video Editor
Copyright (C) 2021 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
***/
#ifndef TIMELINEUNDOABLE_H
#define TIMELINEUNDOABLE_H
#include "config/config.h"
#include "core.h"
#include "node/block/block.h"
#include "node/block/clip/clip.h"
#include "node/block/gap/gap.h"
#include "node/block/transition/transition.h"
#include "node/math/math/math.h"
#include "node/math/merge/merge.h"
#include "node/graph.h"
#include "node/output/track/track.h"
#include "node/output/track/tracklist.h"
#include "timeline/timelinepoints.h"
#include "undo/undocommand.h"
#include "widget/nodeview/nodeviewundo.h"
namespace olive {
inline bool NodeCanBeRemoved(Node* n)
{
return n->output_connections().empty();
}
inline UndoCommand* CreateRemoveCommand(Node* n)
{
return new NodeRemoveWithExclusiveDependenciesAndDisconnect(n);
}
inline UndoCommand* CreateAndRunRemoveCommand(Node* n)
{
UndoCommand* command = CreateRemoveCommand(n);
command->redo();
return command;
}
class BlockResizeCommand : public UndoCommand {
public:
BlockResizeCommand(Block* block, rational new_length) :
block_(block),
new_length_(new_length)
{
}
virtual Project* GetRelevantProject() const override
{
return block_->project();
}
virtual void redo() override
{
old_length_ = block_->length();
block_->set_length_and_media_out(new_length_);
}
virtual void undo() override
{
block_->set_length_and_media_out(old_length_);
}
private:
Block* block_;
rational old_length_;
rational new_length_;
};
class BlockResizeWithMediaInCommand : public UndoCommand {
public:
BlockResizeWithMediaInCommand(Block* block, rational new_length) :
block_(block),
new_length_(new_length)
{
}
virtual Project* GetRelevantProject() const override
{
return block_->project();
}
virtual void redo() override
{
old_length_ = block_->length();
block_->set_length_and_media_in(new_length_);
}
virtual void undo() override
{
block_->set_length_and_media_in(old_length_);
}
private:
Block* block_;
rational old_length_;
rational new_length_;
};
/**
* @brief Performs a trim in the timeline that only affects the block and the block adjacent
*
* Changes the length of one block while also changing the length of the block directly adjacent
* to compensate so that the rest of the track is unaffected.
*
* By default, this will only affect the length of gaps. If the adjacent needs to increase its
* length and is not a gap, a gap will be created and inserted to fill that time. This command can
* be set to always trim even if the adjacent clip isn't a gap with SetTrimIsARollEdit()
*/
class BlockTrimCommand : public UndoCommand {
public:
BlockTrimCommand(Track *track, Block* block, rational new_length, Timeline::MovementMode mode);
virtual ~BlockTrimCommand() override
{
delete deleted_adjacent_command_;
}
virtual Project* GetRelevantProject() const override
{
return track_->project();
}
/**
* @brief Set this if the trim should always affect the adjacent clip and not create a gap
*/
void SetTrimIsARollEdit(bool e)
{
trim_is_a_roll_edit_ = e;
}
/**
* @brief Set whether adjacent blocks set to zero length should be removed from the whole graph
*
* If an adjacent block's length is set to 0, it's automatically removed from the track. By
* default it also gets removed from the whole graph. Set this to FALSE to disable that
* functionality.
*/
void SetRemoveZeroLengthFromGraph(bool e)
{
remove_block_from_graph_ = e;
}
virtual void redo() override;
virtual void undo() override;
private:
void prep();
bool prepped_;
bool doing_nothing_;
rational trim_diff_;
Track* track_;
Block* block_;
rational old_length_;
rational new_length_;
Timeline::MovementMode mode_;
Block* adjacent_;
bool needs_adjacent_;
bool we_created_adjacent_;
bool we_removed_adjacent_;
UndoCommand* deleted_adjacent_command_;
bool trim_is_a_roll_edit_;
bool remove_block_from_graph_;
QObject memory_manager_;
};
class BlockSetMediaInCommand : public UndoCommand {
public:
BlockSetMediaInCommand(Block* block, rational new_media_in) :
block_(block),
new_media_in_(new_media_in)
{
}
virtual Project* GetRelevantProject() const override
{
return block_->project();
}
virtual void redo() override
{
old_media_in_ = block_->media_in();
block_->set_media_in(new_media_in_);
}
virtual void undo() override
{
block_->set_media_in(old_media_in_);
}
private:
Block* block_;
rational old_media_in_;
rational new_media_in_;
};
class TrackRippleRemoveBlockCommand : public UndoCommand {
public:
TrackRippleRemoveBlockCommand(Track* track, Block* block) :
track_(track),
block_(block)
{
}
virtual Project* GetRelevantProject() const override
{
return track_->project();
}
virtual void redo() override
{
before_ = block_->previous();
track_->RippleRemoveBlock(block_);
}
virtual void undo() override
{
track_->InsertBlockAfter(block_, before_);
}
private:
Track* track_;
Block* block_;
Block* before_;
};
class TrackPrependBlockCommand : public UndoCommand {
public:
TrackPrependBlockCommand(Track* track, Block* block) :
track_(track),
block_(block)
{
}
virtual Project* GetRelevantProject() const override
{
return track_->project();
}
virtual void redo() override
{
track_->PrependBlock(block_);
}
virtual void undo() override
{
track_->RippleRemoveBlock(block_);
}
private:
Track* track_;
Block* block_;
};
class TrackInsertBlockAfterCommand : public UndoCommand {
public:
TrackInsertBlockAfterCommand(Track* track, Block* block, Block* before) :
track_(track),
block_(block),
before_(before)
{
}
virtual Project* GetRelevantProject() const override
{
return block_->project();
}
virtual void redo() override
{
track_->InsertBlockAfter(block_, before_);
}
virtual void undo() override
{
track_->RippleRemoveBlock(block_);
}
private:
Track* track_;
Block* block_;
Block* before_;
};
class BlockSplitCommand : public UndoCommand {
public:
BlockSplitCommand(Block* block, rational point) :
block_(block),
new_block_(nullptr),
point_(point),
reconnect_tree_command_(nullptr),
position_command_(nullptr)
{
}
virtual ~BlockSplitCommand() override
{
delete reconnect_tree_command_;
delete position_command_;
}
virtual Project* GetRelevantProject() const override
{
return block_->project();
}
/**
* @brief Access the second block created as a result. Only valid after redo().
*/
Block* new_block()
{
return new_block_;
}
virtual void redo() override
{
old_length_ = block_->length();
Q_ASSERT(point_ > block_->in() && point_ < block_->out());
if (!reconnect_tree_command_) {
reconnect_tree_command_ = new MultiUndoCommand();
new_block_ = static_cast(Node::CopyNodeInGraph(block_, reconnect_tree_command_));
}
reconnect_tree_command_->redo();
// Determine our new lengths
rational new_length = point_ - block_->in();
rational new_part_length = block_->out() - point_;
// Begin an operation
Track* track = block_->track();
track->BeginOperation();
// Set lengths
block_->set_length_and_media_out(new_length);
new_block()->set_length_and_media_in(new_part_length);
// Insert new block
track->InsertBlockAfter(new_block(), block_);
// Position the block
if (!position_command_) {
position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, new_block()->index(), track->Blocks().size(), true);
}
position_command_->redo();
// If the block had an out transition, we move it to the new block
moved_transition_ = NodeInput();
TransitionBlock* potential_transition = dynamic_cast(new_block()->next());
if (potential_transition) {
for (const Node::OutputConnection& output : block_->output_connections()) {
if (output.second.node() == potential_transition) {
moved_transition_ = NodeInput(potential_transition, TransitionBlock::kOutBlockInput);
Node::DisconnectEdge(block_, moved_transition_);
Node::ConnectEdge(new_block(), moved_transition_);
break;
}
}
}
track->EndOperation();
}
virtual void undo() override
{
Track* track = block_->track();
track->BeginOperation();
if (moved_transition_.IsValid()) {
Node::DisconnectEdge(new_block(), moved_transition_);
Node::ConnectEdge(block_, moved_transition_);
}
position_command_->undo();
block_->set_length_and_media_out(old_length_);
track->RippleRemoveBlock(new_block());
// If we ran a reconnect command, disconnect now
reconnect_tree_command_->undo();
track->EndOperation();
}
private:
Block* block_;
Block* new_block_;
rational old_length_;
rational point_;
MultiUndoCommand* reconnect_tree_command_;
NodeInput moved_transition_;
NodeSetPositionAsChildCommand* position_command_;
};
class BlockSplitPreservingLinksCommand : public UndoCommand {
public:
BlockSplitPreservingLinksCommand(const QVector &blocks, const QList& times) :
blocks_(blocks),
times_(times)
{
}
virtual ~BlockSplitPreservingLinksCommand() override
{
qDeleteAll(commands_);
}
virtual Project* GetRelevantProject() const override
{
return blocks_.first()->project();
}
virtual void redo() override
{
if (commands_.isEmpty()) {
QVector< QVector > split_blocks(times_.size());
for (int i=0;i times_.at(i-1));
QVector splits(blocks_.size());
for (int j=0;jin() < time && b->out() > time) {
BlockSplitCommand* split_command = new BlockSplitCommand(b, time);
split_command->redo();
splits.replace(j, split_command->new_block());
commands_.append(split_command);
} else {
splits.replace(j, nullptr);
}
}
split_blocks.replace(i, splits);
}
// Now that we've determined all the splits, we can relink everything
for (int i=0;i& split_list, split_blocks) {
NodeLinkCommand* blc = new NodeLinkCommand(split_list.at(i), split_list.at(j), true);
blc->redo();
commands_.append(blc);
}
}
}
}
} else {
for (int i=0; iredo();
}
}
}
virtual void undo() override
{
for (int i=commands_.size()-1; i>=0; i--) {
commands_.at(i)->undo();
}
}
private:
QVector blocks_;
QList times_;
QVector commands_;
};
class TrackSplitAtTimeCommand : public UndoCommand {
public:
TrackSplitAtTimeCommand(Track* track, rational point) :
prepped_(false),
track_(track),
point_(point),
command_(nullptr)
{
}
virtual ~TrackSplitAtTimeCommand() override
{
delete command_;
}
virtual Project* GetRelevantProject() const override
{
return track_->project();
}
virtual void redo() override
{
if (!prepped_) {
// Find Block that contains this time
Block* b = track_->BlockContainingTime(point_);
if (b) {
command_ = new BlockSplitCommand(b, point_);
}
prepped_ = true;
}
if (command_) {
command_->redo();
}
}
virtual void undo() override
{
if (command_) {
command_->undo();
}
}
private:
bool prepped_;
Track* track_;
rational point_;
UndoCommand* command_;
};
/**
* @brief Clears the area between in and out
*
* The area between `in` and `out` is guaranteed to be freed. BLocks are trimmed and removed to free this space.
* By default, nothing takes this area meaning all subsequent clips are pushed backward, however you can specify
* a block to insert at the `in` point. No checking is done to ensure `insert` is the same length as `in` to `out`.
*/
class TrackRippleRemoveAreaCommand : public UndoCommand {
public:
TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range) :
prepped_(false),
track_(track),
range_(range),
splice_split_command_(nullptr)
{
trim_out_.block = nullptr;
trim_in_.block = nullptr;
}
virtual ~TrackRippleRemoveAreaCommand() override
{
delete splice_split_command_;
qDeleteAll(remove_block_commands_);
}
virtual Project* GetRelevantProject() const override
{
return track_->project();
}
/**
* @brief Block to insert after if you want to insert something between this ripple
*/
Block* GetInsertionIndex() const
{
return insert_previous_;
}
Block* GetSplicedBlock() const
{
if (splice_split_command_) {
return splice_split_command_->new_block();
}
return nullptr;
}
virtual void redo() override
{
if (!prepped_) {
prep();
prepped_ = true;
}
track_->BeginOperation();
if (splice_split_command_) {
// We're just splicing
splice_split_command_->redo();
// Trim the in of the split
Block* split = splice_split_command_->new_block();
split->set_length_and_media_in(split->length() - (range_.out() - split->in()));
} else {
if (trim_out_.block) {
trim_out_.block->set_length_and_media_out(trim_out_.new_length);
}
if (trim_in_.block) {
trim_in_.block->set_length_and_media_in(trim_in_.new_length);
}
// Perform removals
if (!removals_.isEmpty()) {
foreach (auto op, removals_) {
// Ripple remove them all first
track_->RippleRemoveBlock(op.block);
}
// Create undo commands for node removals where possible
if (remove_block_commands_.isEmpty()) {
foreach (auto op, removals_) {
if (NodeCanBeRemoved(op.block)) {
remove_block_commands_.append(CreateRemoveCommand(op.block));
}
}
}
foreach (UndoCommand* c, remove_block_commands_) {
c->redo();
}
}
}
track_->EndOperation();
track_->Node::InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX), Track::kBlockInput);
}
virtual void undo() override
{
// Begin operations
track_->BeginOperation();
if (splice_split_command_) {
splice_split_command_->undo();
} else {
if (trim_out_.block) {
trim_out_.block->set_length_and_media_out(trim_out_.old_length);
}
if (trim_in_.block) {
trim_in_.block->set_length_and_media_in(trim_in_.old_length);
}
// Un-remove any blocks
for (int i=remove_block_commands_.size()-1; i>=0; i--) {
remove_block_commands_.at(i)->undo();
}
foreach (auto op, removals_) {
track_->InsertBlockAfter(op.block, op.before);
}
}
// End operations and invalidate
track_->EndOperation();
track_->Node::InvalidateCache(TimeRange(range_.in(), RATIONAL_MAX), Track::kBlockInput);
}
private:
bool prepped_;
void prep()
{
// Determine precisely what will be happening to these tracks
Block* first_block = track_->NearestBlockBeforeOrAt(range_.in());
if (!first_block) {
// No blocks at this time, nothing to be done on this track
return;
}
// Determine if this first block is getting trimmed or removed
bool first_block_is_out_trimmed = first_block->in() < range_.in();
bool first_block_is_in_trimmed = first_block->out() > range_.out();
// Set's the block that any insert command should insert AFTER. If the first block is not
// getting out-trimmed, that means first block is either getting removed or in-trimmed, which
// means any insert should happen before it
insert_previous_ = first_block_is_out_trimmed ? first_block : first_block->previous();
// If it's getting trimmed, determine if it's actually getting spliced
if (first_block_is_out_trimmed && first_block_is_in_trimmed) {
// This block is getting spliced, so we'll handle that later
splice_split_command_ = new BlockSplitCommand(first_block, range_.in());
} else {
// It's just getting trimmed or removed, so we'll append that operation
if (first_block_is_out_trimmed) {
trim_out_ = {first_block,
first_block->length(),
first_block->length() - (first_block->out() - range_.in())};
} else if (first_block_is_in_trimmed) {
// Block is getting in trimmed
trim_in_ = {first_block,
first_block->length(),
first_block->length() - (range_.out() - first_block->in())};
} else {
// We know for sure this block is within the range so it will be removed
removals_.append(RemoveOperation({first_block, first_block->previous()}));
}
// If the first block is getting in trimmed, we're already at the end of our range
if (!first_block_is_in_trimmed) {
// Loop through the rest of the blocks and determine what to do with those
for (Block* next=first_block->next(); next; next=next->next()) {
bool trimming = (next->out() > range_.out());
if (trimming) {
trim_in_ = {next,
next->length(),
next->length() - (range_.out() - next->in())};
break;
} else {
removals_.append(RemoveOperation({next, next->previous()}));
if (next->out() == range_.out()) {
break;
}
}
}
}
}
}
struct TrimOperation {
Block* block;
rational old_length;
rational new_length;
};
struct RemoveOperation {
Block* block;
Block* before;
};
Track* track_;
TimeRange range_;
TrimOperation trim_out_;
QVector removals_;
TrimOperation trim_in_;
Block* insert_previous_;
BlockSplitCommand* splice_split_command_;
QVector remove_block_commands_;
};
class TrackListRippleRemoveAreaCommand : public UndoCommand {
public:
TrackListRippleRemoveAreaCommand(TrackList* list, rational in, rational out) :
list_(list),
in_(in),
out_(out)
{
}
virtual ~TrackListRippleRemoveAreaCommand() override
{
qDeleteAll(commands_);
}
virtual Project* GetRelevantProject() const override
{
return list_->parent()->project();
}
virtual void redo() override
{
// Code that's only run on the first redo
if (commands_.isEmpty()) {
all_tracks_unlocked_ = true;
foreach (Track* track, list_->GetTracks()) {
if (track->IsLocked()) {
all_tracks_unlocked_ = false;
continue;
}
TrackRippleRemoveAreaCommand* c = new TrackRippleRemoveAreaCommand(track, TimeRange(in_, out_));
commands_.append(c);
working_tracks_.append(track);
}
}
if (all_tracks_unlocked_) {
// We can optimize here by simply shifting the whole cache forward instead of re-caching
// everything following this time
if (list_->type() == Track::kVideo) {
list_->parent()->ShiftVideoCache(out_, in_);
} else if (list_->type() == Track::kAudio) {
list_->parent()->ShiftAudioCache(out_, in_);
}
foreach (Track* track, working_tracks_) {
track->BeginOperation();
}
}
foreach (TrackRippleRemoveAreaCommand* c, commands_) {
c->redo();
}
if (all_tracks_unlocked_) {
foreach (Track* track, working_tracks_) {
track->EndOperation();
}
}
}
virtual void undo() override
{
if (all_tracks_unlocked_) {
// We can optimize here by simply shifting the whole cache forward instead of re-caching
// everything following this time
if (list_->type() == Track::kVideo) {
list_->parent()->ShiftVideoCache(in_, out_);
} else if (list_->type() == Track::kAudio) {
list_->parent()->ShiftAudioCache(in_, out_);
}
foreach (Track* track, working_tracks_) {
track->BeginOperation();
}
}
foreach (TrackRippleRemoveAreaCommand* c, commands_) {
c->undo();
}
if (all_tracks_unlocked_) {
foreach (Track* track, working_tracks_) {
track->EndOperation();
}
}
}
private:
TrackList* list_;
QList