/***
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 .
***/
#include "timelineundogeneral.h"
#include "node/block/clip/clip.h"
#include "node/block/transition/transition.h"
#include "node/math/math/math.h"
#include "node/math/merge/merge.h"
#include "timelineundocommon.h"
namespace olive {
//
// BlockResizeCommand
//
void BlockResizeCommand::redo()
{
old_length_ = block_->length();
block_->set_length_and_media_out(new_length_);
}
void BlockResizeCommand::undo()
{
block_->set_length_and_media_out(old_length_);
}
//
// BlockResizeWithMediaInCommand
//
void BlockResizeWithMediaInCommand::redo()
{
old_length_ = block_->length();
block_->set_length_and_media_in(new_length_);
}
void BlockResizeWithMediaInCommand::undo()
{
block_->set_length_and_media_in(old_length_);
}
//
// BlockSetMediaInCommand
//
void BlockSetMediaInCommand::redo()
{
old_media_in_ = block_->media_in();
block_->set_media_in(new_media_in_);
}
void BlockSetMediaInCommand::undo()
{
block_->set_media_in(old_media_in_);
}
//
// TimelineAddTrackCommand
//
TimelineAddTrackCommand::TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks)
{
timeline_ = timeline;
position_command_ = nullptr;
track_ = new Track();
track_->setParent(&memory_manager_);
if (timeline->GetTrackCount() > 0 && automerge_tracks) {
if (timeline_->type() == Track::kVideo) {
merge_ = new MergeNode();
base_ = NodeInput(merge_, MergeNode::kBaseIn);
blend_ = NodeInput(merge_, MergeNode::kBlendIn);
} else if (timeline_->type() == Track::kAudio) {
merge_ = new MathNode();
base_ = NodeInput(merge_, MathNode::kParamAIn);
blend_ = NodeInput(merge_, MathNode::kParamBIn);
} else {
merge_ = nullptr;
}
} else {
merge_ = nullptr;
}
if (merge_) {
merge_->setParent(&memory_manager_);
}
}
void TimelineAddTrackCommand::redo()
{
// Add track
track_->setParent(timeline_->GetParentGraph());
timeline_->ArrayAppend();
Node::ConnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1));
// Add merge if applicable
Track* last_track = nullptr;
if (merge_) {
merge_->setParent(timeline_->GetParentGraph());
last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2);
// Whatever this track used to be connected to, connect the merge instead
const Node::OutputConnections edges = last_track->output_connections();
for (const Node::OutputConnection& ic : edges) {
const NodeInput& i = ic.second;
// Ignore the track input, but funnel everything else through our merge
if (i.node() != timeline_->parent() || i.input() != timeline_->track_input()) {
Node::DisconnectEdge(last_track, i);
Node::ConnectEdge(merge_, i);
}
}
// Connect this as the "blend" track
Node::ConnectEdge(track_, blend_);
Node::ConnectEdge(last_track, base_);
} else if (timeline_->GetTrackCount() == 1) {
// If this was the first track we added,
QString relevant_input;
if (timeline_->type() == Track::kVideo) {
relevant_input = ViewerOutput::kTextureInput;
} else if (timeline_->type() == Track::kAudio) {
relevant_input = ViewerOutput::kSamplesInput;
}
if (!relevant_input.isEmpty() && !timeline_->parent()->IsInputConnected(relevant_input)) {
direct_ = NodeInput(timeline_->parent(), relevant_input);
Node::ConnectEdge(track_, direct_);
} else {
direct_ = NodeInput();
}
}
// Position track in context
if (!position_command_) {
int track_count = timeline_->parent()->GetTracks().size();
position_command_ = new MultiUndoCommand();
// Position either the merge or the track as an "element"
Node *node_to_position = merge_ ? merge_ : track_;
double node_index = track_count - 1;
if (merge_) {
node_index -= 1;
position_command_->add_child(new NodeRemovePositionFromContextCommand(last_track, timeline_->parent()));
}
position_command_->add_child(new NodeSetPositionAsChildCommand(node_to_position, timeline_->parent(), timeline_->parent(), node_index, track_count, true));
// If we positioned a merge, position the tracks as children of the merge
if (merge_) {
// `last_track` should be non-null if `merge_` is non-null
position_command_->add_child(new NodeSetPositionAsChildCommand(last_track, merge_, timeline_->parent(), 0, 2, true));
position_command_->add_child(new NodeSetPositionAsChildCommand(track_, merge_, timeline_->parent(), 1, 2, true));
}
}
position_command_->redo_now();
}
void TimelineAddTrackCommand::undo()
{
position_command_->undo_now();
// Remove merge if applicable
if (merge_) {
// Assume whatever this merge is connected to USED to be connected to the last track
Track* last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2);
Node::DisconnectEdge(track_, blend_);
Node::DisconnectEdge(last_track, base_);
// Make copy of edges since the node's internal array will change as we disconnect things
const Node::OutputConnections edges = merge_->output_connections();
for (const Node::OutputConnection& ic : edges) {
const NodeInput& i = ic.second;
Node::DisconnectEdge(merge_, i);
Node::ConnectEdge(last_track, i);
}
merge_->setParent(&memory_manager_);
} else if (direct_.IsValid()) {
Node::DisconnectEdge(track_, direct_);
}
// Remove track
Node::DisconnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1));
timeline_->ArrayRemoveLast();
track_->setParent(&memory_manager_);
}
//
// TransitionRemoveCommand
//
void TransitionRemoveCommand::redo()
{
track_ = block_->track();
out_block_ = block_->connected_out_block();
in_block_ = block_->connected_in_block();
Q_ASSERT(out_block_ || in_block_);
track_->BeginOperation();
TimeRange invalidate_range(block_->in(), block_->out());
if (in_block_) {
in_block_->set_length_and_media_in(in_block_->length() + block_->in_offset());
}
if (out_block_) {
out_block_->set_length_and_media_out(out_block_->length() + block_->out_offset());
}
if (in_block_) {
Node::DisconnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput));
}
if (out_block_) {
Node::DisconnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput));
}
track_->RippleRemoveBlock(block_);
track_->EndOperation();
track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput);
if (remove_from_graph_) {
if (!remove_command_) {
remove_command_ = CreateRemoveCommand(block_);
}
remove_command_->redo_now();
}
}
void TransitionRemoveCommand::undo()
{
if (remove_from_graph_) {
remove_command_->undo_now();
}
track_->BeginOperation();
if (in_block_) {
track_->InsertBlockBefore(block_, in_block_);
} else {
track_->InsertBlockAfter(block_, out_block_);
}
if (in_block_) {
Node::ConnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput));
}
if (out_block_) {
Node::ConnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput));
}
// These if statements must be separated because in_offset and out_offset report different things
// if only one block is connected vs two. So we have to connect the blocks first before we have
// an accurate return value from these offset functions.
if (in_block_) {
in_block_->set_length_and_media_in(in_block_->length() - block_->in_offset());
}
if (out_block_) {
out_block_->set_length_and_media_out(out_block_->length() - block_->out_offset());
}
track_->EndOperation();
track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput);
}
//
// TrackListInsertGaps
//
void TrackListInsertGaps::prepare()
{
// Determine if all tracks will be affected, which will allow us to make some optimizations
all_tracks_unlocked_ = true;
foreach (Track* track, track_list_->GetTracks()) {
if (track->IsLocked()) {
all_tracks_unlocked_ = false;
continue;
}
working_tracks_.append(track);
}
QVector blocks_to_split;
QVector blocks_to_append_gap_to;
QVector