/*** 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 "timelineundoripple.h" #include "timelineundocommon.h" namespace olive { // // TrackRippleRemoveAreaCommand // TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand( Track *track, const TimeRange &range) : track_(track) , range_(range) , allow_splitting_gaps_(false) , splice_split_command_(nullptr) { trim_out_.block = nullptr; trim_in_.block = nullptr; } TrackRippleRemoveAreaCommand::~TrackRippleRemoveAreaCommand() { delete splice_split_command_; qDeleteAll(remove_block_commands_); } void TrackRippleRemoveAreaCommand::prepare() { // 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) { if (!allow_splitting_gaps_ && dynamic_cast(first_block)) { // As a rule, we don't split gaps, so we just treat it as a trim of the range requested trim_out_ = { first_block, first_block->length(), first_block->length() - range_.length() }; } else { // 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; } } } } } } void TrackRippleRemoveAreaCommand::redo() { if (splice_split_command_) { // We're just splicing splice_split_command_->redo_now(); // 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_now(); } } } } void TrackRippleRemoveAreaCommand::undo() { if (splice_split_command_) { splice_split_command_->undo_now(); } 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_now(); } foreach (auto op, removals_) { track_->InsertBlockAfter(op.block, op.before); } } } // // TrackListRippleRemoveAreaCommand // void TrackListRippleRemoveAreaCommand::prepare() { foreach (Track *track, list_->GetTracks()) { if (track->IsLocked()) { continue; } TrackRippleRemoveAreaCommand *c = new TrackRippleRemoveAreaCommand(track, range_); commands_.append(c); working_tracks_.append(track); } } void TrackListRippleRemoveAreaCommand::redo() { foreach (TrackRippleRemoveAreaCommand *c, commands_) { c->redo_now(); } } void TrackListRippleRemoveAreaCommand::undo() { foreach (TrackRippleRemoveAreaCommand *c, commands_) { c->undo_now(); } } // // TimelineRippleRemoveAreaCommand // TimelineRippleRemoveAreaCommand::TimelineRippleRemoveAreaCommand( Sequence *timeline, rational in, rational out) : timeline_(timeline) { for (int i = 0; i < Track::kCount; i++) { add_child(new TrackListRippleRemoveAreaCommand( timeline->track_list(static_cast(i)), in, out)); } } // // TrackListRippleToolCommand // TrackListRippleToolCommand::TrackListRippleToolCommand( TrackList *track_list, const QHash &info, const rational &ripple_movement, const Timeline::MovementMode &movement_mode) : track_list_(track_list) , info_(info) , ripple_movement_(ripple_movement) , movement_mode_(movement_mode) { } void TrackListRippleToolCommand::ripple(bool redo) { if (info_.isEmpty()) { return; } // The following variables are used to determine how much of the cache to invalidate // If we can shift, we will shift from the latest out before the ripple to the latest out after, // since those sections will be unchanged by this ripple rational pre_latest_out = RATIONAL_MIN; rational post_latest_out = RATIONAL_MIN; // Make timeline changes for (auto it = info_.cbegin(); it != info_.cend(); it++) { Track *track = it.key(); const RippleInfo &info = it.value(); WorkingData working_data = working_data_.value(track); Block *b = info.block; // Generate block length rational new_block_length; rational operation_movement = ripple_movement_; if (movement_mode_ == Timeline::kTrimIn) { operation_movement = -operation_movement; } if (!redo) { operation_movement = -operation_movement; } if (b) { new_block_length = b->length() + operation_movement; } rational pre_shift; rational post_shift; if (info.append_gap) { // Rather than rippling the referenced block, we'll insert a gap and ripple with that GapBlock *gap = working_data.created_gap; if (redo) { if (!gap) { gap = new GapBlock(); gap->set_length_and_media_out(qAbs(ripple_movement_)); working_data.created_gap = gap; } gap->setParent(track->parent()); track->InsertBlockBefore(gap, b); // As an insertion, we will shift from the gap's in to the gap's out pre_shift = gap->in(); post_shift = gap->out(); working_data.earliest_point_of_change = gap->in(); } else { // As a removal, we will shift from the gap's out to the gap's in pre_shift = gap->out(); post_shift = gap->in(); track->RippleRemoveBlock(gap); gap->setParent(&memory_manager_); } } else if ((redo && new_block_length.isNull()) || (!redo && !b->track())) { // The ripple is the length of this block. We assume that for this to happen, it must have // been a gap that we will now remove. if (redo) { // The earliest point changes will happen is at the start of this block working_data.earliest_point_of_change = b->in(); // As a removal, we will be shifting from the out point to the in point pre_shift = b->out(); post_shift = b->in(); // Remove gap from track and from graph working_data.removed_gap_after = b->previous(); track->RippleRemoveBlock(b); b->setParent(&memory_manager_); } else { // Restore gap to graph and track b->setParent(track->parent()); track->InsertBlockAfter(b, working_data.removed_gap_after); // The earliest point changes will happen is at the start of this block working_data.earliest_point_of_change = b->in(); // As an insert, we will be shifting from the block's in point to its out point pre_shift = b->in(); post_shift = b->out(); } } else { // Store old length working_data.old_length = b->length(); if (movement_mode_ == Timeline::kTrimIn) { // The earliest point changes will occur is in point of this bloc working_data.earliest_point_of_change = b->in(); // Undo the trim in inversion we do above, this will still be inverted accurately for // undoing where appropriate rational inverted = -operation_movement; if (inverted > 0) { pre_shift = b->in() + inverted; post_shift = b->in(); } else { pre_shift = b->in(); post_shift = b->in() - inverted; } // Update length b->set_length_and_media_in(new_block_length); } else { // The earliest point changes will occur is the out point if trimming out or the in point // if trimming in working_data.earliest_point_of_change = b->out(); // The latest out before the ripple is this block's current out point pre_shift = b->out(); // Update length b->set_length_and_media_out(new_block_length); // The latest out after the ripple is this block's out point after the length change post_shift = b->out(); } } working_data_.insert(it.key(), working_data); pre_latest_out = qMax(pre_latest_out, pre_shift); post_latest_out = qMax(post_latest_out, post_shift); } } // // TimelineRippleDeleteGapsAtRegionsCommand // void TimelineRippleDeleteGapsAtRegionsCommand::prepare() { int max_gaps = 0; QHash> requested_gaps; // Convert regions to gaps for (const QPair ®ion : qAsConst(regions_)) { Track *track = region.first; const TimeRange &range = region.second; GapBlock *gap = dynamic_cast(track->NearestBlockBeforeOrAt(range.in())); if (gap) { QVector &gaps_on_track = requested_gaps[track]; RemovalRequest this_req = { gap, range }; // Insertion sort bool inserted = false; for (int i = 0; i < gaps_on_track.size(); i++) { if (gaps_on_track.at(i).range.in() < range.in()) { gaps_on_track.insert(i, this_req); inserted = true; break; } } if (!inserted) { gaps_on_track.append(this_req); } max_gaps = qMax(max_gaps, gaps_on_track.size()); } else { qWarning() << "Failed to find corresponding gap to region"; } } // For each gap on each track, find a corresponding gap on every other track (which may include // a requested gap) to ripple in order to keep everything synchronized QHash gap_lengths; for (int gap_index = 0; gap_index < max_gaps; gap_index++) { rational earliest_point = RATIONAL_MAX; rational ripple_length = RATIONAL_MAX; rational latest_point = RATIONAL_MIN; foreach (const QVector &gaps_on_track, requested_gaps) { if (gap_index < gaps_on_track.size()) { const RemovalRequest &gap = gaps_on_track.at(gap_index); earliest_point = qMin(earliest_point, gap.range.in()); ripple_length = qMin(ripple_length, gap.range.length()); latest_point = qMax(latest_point, gap.range.out()); } } // Determine which gaps will be involved in this operation QVector gaps; foreach (Track *track, timeline_->GetTracks()) { if (track->IsLocked()) { continue; } const QVector &requested_gaps_on_track = requested_gaps.value(track); GapBlock *gap = nullptr; if (gap_index < requested_gaps_on_track.size()) { // A requested gap was at this index, use it gap = requested_gaps_on_track.at(gap_index).gap; } else { // No requested gap was at this index, find one Block *block = track->NearestBlockAfterOrAt(earliest_point); if (block) { // Found a block, test if it's a gap gap = dynamic_cast(block); if (!gap) { if (block->in() == earliest_point) { if (block->next()) { gap = dynamic_cast(block->next()); if (!gap) { ripple_length = 0; } } } else { gap = dynamic_cast(block->previous()); if (!gap) { ripple_length = 0; } } } } else { // Assume track finishes here and track won't be affected by this operation } } if (gap) { gaps.append(gap); if (!gap_lengths.contains(gap)) { gap_lengths.insert(gap, gap->length()); } ripple_length = qMin(ripple_length, gap_lengths.value(gap)); } if (ripple_length == 0) { break; } } if (ripple_length > 0) { foreach (GapBlock *gap, gaps) { if (gap_lengths.value(gap) == ripple_length) { commands_.append( new TrackRippleRemoveBlockCommand(gap->track(), gap)); } else { gap_lengths[gap] -= ripple_length; commands_.append( new BlockResizeCommand(gap, gap_lengths.value(gap))); } } } } } void TimelineRippleDeleteGapsAtRegionsCommand::redo() { for (auto it = commands_.cbegin(); it != commands_.cend(); it++) { (*it)->redo_now(); } } void TimelineRippleDeleteGapsAtRegionsCommand::undo() { for (auto it = commands_.crbegin(); it != commands_.crend(); it++) { (*it)->undo_now(); } } }