/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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/factory.h"
#include "node/math/math/math.h"
#include "node/math/merge/merge.h"
#include "timelineundocommon.h"
#include "timelineundotrack.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)
, merge_(nullptr)
, position_command_(nullptr)
{
// Create new track
track_ = new Track();
track_->setParent(&memory_manager_);
// Determine what input to connect it to
QString relevant_input;
if (timeline_->type() == Track::k_video) {
relevant_input = Sequence::k_texture_input;
} else if (timeline_->type() == Track::k_audio) {
relevant_input = Sequence::k_samples_input;
}
// If we have an input to connect to, set it as our `direct` connection
if (!relevant_input.isEmpty()) {
direct_ = NodeInput(timeline_->parent(), relevant_input);
// If we're automerging and something is already connected, determine if/how to merge it
if (automerge_tracks && direct_.is_connected()) {
if (timeline_->type() == Track::k_video) {
// Use merge for video
merge_ = new MergeNode();
base_ = NodeInput(merge_, MergeNode::k_base_in);
blend_ = NodeInput(merge_, MergeNode::k_blend_in);
} else if (timeline_->type() == Track::k_audio) {
// Use math (add) for audio
merge_ = new MathNode();
base_ = NodeInput(merge_, MathNode::k_param_a_in);
blend_ = NodeInput(merge_, MathNode::k_param_b_in);
}
if (merge_) {
// If we got created a merge node, ensure it's parented
merge_->setParent(&memory_manager_);
}
}
}
}
void TimelineAddTrackCommand::redo()
{
// Get sequence
Sequence *sequence = timeline_->parent();
// Add track to sequence
track_->setParent(timeline_->get_parent_graph());
if (timeline_->get_track_count() > 0) {
track_->set_track_height(
timeline_->get_track_at(timeline_->get_track_count() - 1)
->get_track_height());
}
timeline_->array_append();
Node::connect_edge(track_,
timeline_->track_input(timeline_->array_size() - 1));
qreal position_factor = 0.5;
if (timeline_->type() == Track::k_video) {
position_factor = -position_factor;
}
bool create_pos_command =
(!position_command_ && (timeline_->type() == Track::k_video ||
timeline_->type() == Track::k_audio));
if (create_pos_command) {
position_command_ = new MultiUndoCommand();
}
// Add merge if applicable
if (merge_) {
// Determine what was previously connected
Node *previous_connection = direct_.get_connected_output();
// Add merge to graph
merge_->setParent(timeline_->get_parent_graph());
// Connect merge between what used to be here
Node::disconnect_edge(previous_connection, direct_);
Node::connect_edge(merge_, direct_);
Node::connect_edge(previous_connection, base_);
Node::connect_edge(track_, blend_);
if (create_pos_command) {
position_command_->add_child(new NodeSetPositionCommand(
track_, sequence,
sequence->get_node_position_in_context(sequence) +
QPointF(-1, -position_factor)));
position_command_->add_child(new NodeSetPositionCommand(
merge_, sequence,
sequence->get_node_position_in_context(sequence)));
position_command_->add_child(
new NodeSetPositionAndDependenciesRecursivelyCommand(
merge_, sequence,
sequence->get_node_position_in_context(sequence) +
QPointF(-1,
position_factor * timeline_->get_track_count())));
}
} else if (direct_.is_valid() && !direct_.is_connected()) {
// If no merge, we have a direct connection, and nothing else is connected, connect this
Node::connect_edge(track_, direct_);
if (create_pos_command) {
// Just position directly next to the context node
position_command_->add_child(new NodeSetPositionCommand(
track_, sequence,
sequence->get_node_position_in_context(sequence) +
QPointF(-1, position_factor)));
}
}
// Run position command if we created one
if (position_command_) {
position_command_->redo_now();
}
}
void TimelineAddTrackCommand::undo()
{
if (position_command_) {
position_command_->undo_now();
}
// Remove merge if applicable
if (merge_) {
Node *previous_connection = base_.get_connected_output();
Node::disconnect_edge(track_, blend_);
Node::disconnect_edge(previous_connection, base_);
Node::disconnect_edge(merge_, direct_);
Node::connect_edge(previous_connection, direct_);
merge_->setParent(&memory_manager_);
} else if (direct_.is_valid() && direct_.get_connected_output() == track_) {
Node::disconnect_edge(track_, direct_);
}
// Remove track
Node::disconnect_edge(track_,
timeline_->track_input(timeline_->array_size() - 1));
timeline_->array_remove_last();
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_);
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::disconnect_edge(in_block_,
NodeInput(block_, TransitionBlock::k_in_block_input));
}
if (out_block_) {
Node::disconnect_edge(
out_block_, NodeInput(block_, TransitionBlock::k_out_block_input));
}
track_->ripple_remove_block(block_);
if (remove_from_graph_) {
if (!remove_command_) {
remove_command_ = create_remove_command(block_);
}
remove_command_->redo_now();
}
}
void TransitionRemoveCommand::undo()
{
if (remove_from_graph_) {
remove_command_->undo_now();
}
if (in_block_) {
track_->insert_block_before(block_, in_block_);
} else {
track_->insert_block_after(block_, out_block_);
}
if (in_block_) {
Node::connect_edge(in_block_,
NodeInput(block_, TransitionBlock::k_in_block_input));
}
if (out_block_) {
Node::connect_edge(out_block_,
NodeInput(block_, TransitionBlock::k_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());
}
}
//
// TrackListInsertGaps
//
void TrackListInsertGaps::prepare()
{
// Determine if all tracks will be affected, which will allow us to make some optimizations
foreach (Track *track, track_list_->get_tracks()) {
if (track->is_locked()) {
continue;
}
working_tracks_.append(track);
}
QVector blocks_to_split;
QVector blocks_to_append_gap_to;
QVector