From c41047ab0233c2aaac4f027aa2412b48edd4fd7c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 16 Jul 2021 01:38:20 -0700 Subject: [PATCH] timeline: reorganized undo commands Split commands between several files rather than having them all in two monolithic ones. --- app/node/project/sequence/sequence.cpp | 1 + app/task/project/loadotio/loadotio.cpp | 3 +- app/undo/undocommand.cpp | 10 + app/undo/undocommand.h | 9 +- app/widget/nodeview/nodeviewundo.cpp | 1 - app/widget/timebased/timebasedwidget.cpp | 2 +- app/widget/timelinewidget/CMakeLists.txt | 3 +- app/widget/timelinewidget/timelineundo.cpp | 373 --- app/widget/timelinewidget/timelineundo.h | 2228 ----------------- app/widget/timelinewidget/timelinewidget.cpp | 4 + app/widget/timelinewidget/tool/add.cpp | 1 + app/widget/timelinewidget/tool/import.cpp | 1 + app/widget/timelinewidget/tool/pointer.cpp | 1 + app/widget/timelinewidget/tool/razor.cpp | 1 + app/widget/timelinewidget/tool/ripple.cpp | 1 + app/widget/timelinewidget/tool/slip.cpp | 1 + app/widget/timelinewidget/tool/tool.h | 1 - app/widget/timelinewidget/tool/transition.cpp | 2 +- app/widget/timelinewidget/undo/CMakeLists.txt | 33 + .../timelinewidget/undo/timelineundocommon.h | 48 + .../undo/timelineundogeneral.cpp | 611 +++++ .../timelinewidget/undo/timelineundogeneral.h | 325 +++ .../undo/timelineundopointer.cpp | 439 ++++ .../timelinewidget/undo/timelineundopointer.h | 204 ++ .../undo/timelineundoripple.cpp | 500 ++++ .../timelinewidget/undo/timelineundoripple.h | 237 ++ .../timelinewidget/undo/timelineundosplit.cpp | 181 ++ .../timelinewidget/undo/timelineundosplit.h | 159 ++ .../timelinewidget/undo/timelineundotrack.cpp | 25 + .../timelinewidget/undo/timelineundotrack.h | 163 ++ .../undo/timelineundoworkarea.cpp | 25 + .../undo/timelineundoworkarea.h | 103 + tests/timeline/timeline-tests.cpp | 3 +- 33 files changed, 3088 insertions(+), 2611 deletions(-) delete mode 100644 app/widget/timelinewidget/timelineundo.cpp delete mode 100644 app/widget/timelinewidget/timelineundo.h create mode 100644 app/widget/timelinewidget/undo/CMakeLists.txt create mode 100644 app/widget/timelinewidget/undo/timelineundocommon.h create mode 100644 app/widget/timelinewidget/undo/timelineundogeneral.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundogeneral.h create mode 100644 app/widget/timelinewidget/undo/timelineundopointer.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundopointer.h create mode 100644 app/widget/timelinewidget/undo/timelineundoripple.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundoripple.h create mode 100644 app/widget/timelinewidget/undo/timelineundosplit.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundosplit.h create mode 100644 app/widget/timelinewidget/undo/timelineundotrack.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundotrack.h create mode 100644 app/widget/timelinewidget/undo/timelineundoworkarea.cpp create mode 100644 app/widget/timelinewidget/undo/timelineundoworkarea.h diff --git a/app/node/project/sequence/sequence.cpp b/app/node/project/sequence/sequence.cpp index 6ee484adc..4d13704b2 100644 --- a/app/node/project/sequence/sequence.cpp +++ b/app/node/project/sequence/sequence.cpp @@ -24,6 +24,7 @@ #include "panel/timeline/timeline.h" #include "ui/icons/icons.h" +#include "widget/timelinewidget/undo/timelineundogeneral.h" namespace olive { diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp index 0d817104a..dd4428613 100644 --- a/app/task/project/loadotio/loadotio.cpp +++ b/app/task/project/loadotio/loadotio.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "node/block/clip/clip.h" @@ -36,7 +37,7 @@ #include "node/project/folder/folder.h" #include "node/project/footage/footage.h" #include "node/project/sequence/sequence.h" -#include "widget/timelinewidget/timelineundo.h" +#include "widget/timelinewidget/undo/timelineundogeneral.h" namespace olive { diff --git a/app/undo/undocommand.cpp b/app/undo/undocommand.cpp index 668617b65..4cf60da80 100644 --- a/app/undo/undocommand.cpp +++ b/app/undo/undocommand.cpp @@ -49,8 +49,18 @@ void MultiUndoCommand::undo() } } +UndoCommand::UndoCommand() +{ + prepared_ = false; +} + void UndoCommand::redo_and_set_modified() { + if (!prepared_) { + prepare(); + prepared_ = true; + } + redo(); project_ = GetRelevantProject(); diff --git a/app/undo/undocommand.h b/app/undo/undocommand.h index c310bf131..f7fb98579 100644 --- a/app/undo/undocommand.h +++ b/app/undo/undocommand.h @@ -34,17 +34,20 @@ class Project; class UndoCommand { public: - UndoCommand() = default; + UndoCommand(); virtual ~UndoCommand(){} DISABLE_COPY_MOVE(UndoCommand) + virtual void prepare(){} virtual void redo() = 0; virtual void undo() = 0; - void redo_and_set_modified(); + bool has_prepared() const {return prepared_;} + void set_prepared(bool e) {prepared_ = true;} + void redo_and_set_modified(); void undo_and_set_modified(); virtual Project* GetRelevantProject() const = 0; @@ -66,6 +69,8 @@ private: Project* project_; + bool prepared_; + }; class MultiUndoCommand : public UndoCommand diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index 565a4d535..ca0a37b8a 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -21,7 +21,6 @@ #include "nodeviewundo.h" #include "node/project/sequence/sequence.h" -#include "widget/timelinewidget/timelineundo.h" namespace olive { diff --git a/app/widget/timebased/timebasedwidget.cpp b/app/widget/timebased/timebasedwidget.cpp index 6385a89c4..86befc724 100644 --- a/app/widget/timebased/timebasedwidget.cpp +++ b/app/widget/timebased/timebasedwidget.cpp @@ -27,7 +27,7 @@ #include "config/config.h" #include "core.h" #include "node/project/sequence/sequence.h" -#include "widget/timelinewidget/timelineundo.h" +#include "widget/timelinewidget/undo/timelineundoworkarea.h" namespace olive { diff --git a/app/widget/timelinewidget/CMakeLists.txt b/app/widget/timelinewidget/CMakeLists.txt index 04312d89d..4de23d7da 100644 --- a/app/widget/timelinewidget/CMakeLists.txt +++ b/app/widget/timelinewidget/CMakeLists.txt @@ -16,14 +16,13 @@ add_subdirectory(trackview) add_subdirectory(tool) +add_subdirectory(undo) add_subdirectory(view) set(OLIVE_SOURCES ${OLIVE_SOURCES} widget/timelinewidget/timelineandtrackview.cpp widget/timelinewidget/timelineandtrackview.h - widget/timelinewidget/timelineundo.cpp - widget/timelinewidget/timelineundo.h widget/timelinewidget/timelinewidget.cpp widget/timelinewidget/timelinewidget.h widget/timelinewidget/timelinewidgetselections.cpp diff --git a/app/widget/timelinewidget/timelineundo.cpp b/app/widget/timelinewidget/timelineundo.cpp deleted file mode 100644 index 0f9aa229a..000000000 --- a/app/widget/timelinewidget/timelineundo.cpp +++ /dev/null @@ -1,373 +0,0 @@ -/*** - - 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 "timelineundo.h" - -namespace olive { - -BlockTrimCommand::BlockTrimCommand(Track *track, Block* block, rational new_length, Timeline::MovementMode mode) : - prepped_(false), - track_(track), - block_(block), - new_length_(new_length), - mode_(mode), - deleted_adjacent_command_(nullptr), - trim_is_a_roll_edit_(false) -{ -} - -void BlockTrimCommand::redo() -{ - if (!prepped_) { - prep(); - prepped_ = true; - } - - if (doing_nothing_) { - return; - } - - // Begin an operation since we'll be doing a lot - track_->BeginOperation(); - - // Determine how much time to invalidate - TimeRange invalidate_range; - - if (mode_ == Timeline::kTrimIn) { - invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff_); - block_->set_length_and_media_in(new_length_); - } else { - invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff_); - block_->set_length_and_media_out(new_length_); - } - - if (needs_adjacent_) { - if (we_created_adjacent_) { - // Add adjacent and insert it - adjacent_->setParent(track_->parent()); - - if (mode_ == Timeline::kTrimIn) { - track_->InsertBlockBefore(adjacent_, block_); - } else { - track_->InsertBlockAfter(adjacent_, block_); - } - } else if (we_removed_adjacent_) { - track_->RippleRemoveBlock(adjacent_); - - // It no longer inputs/outputs anything, remove it - if (remove_block_from_graph_ && NodeCanBeRemoved(adjacent_)) { - if (!deleted_adjacent_command_) { - deleted_adjacent_command_ = CreateAndRunRemoveCommand(adjacent_); - } else { - deleted_adjacent_command_->redo(); - } - } - } else { - rational adjacent_length = adjacent_->length() + trim_diff_; - - if (mode_ == Timeline::kTrimIn) { - adjacent_->set_length_and_media_out(adjacent_length); - } else { - adjacent_->set_length_and_media_in(adjacent_length); - } - } - } - - track_->EndOperation(); - - if (dynamic_cast(block_)) { - // Whole transition needs to be invalidated - invalidate_range = block_->range(); - } - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); -} - -void BlockTrimCommand::undo() -{ - if (doing_nothing_) { - return; - } - - track_->BeginOperation(); - - // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer - if (needs_adjacent_) { - if (we_created_adjacent_) { - // Adjacent is ours, just delete it - track_->RippleRemoveBlock(adjacent_); - adjacent_->setParent(&memory_manager_); - } else { - if (we_removed_adjacent_) { - if (deleted_adjacent_command_) { - // We deleted adjacent, restore it now - deleted_adjacent_command_->undo(); - } - - if (mode_ == Timeline::kTrimIn) { - track_->InsertBlockBefore(adjacent_, block_); - } else { - track_->InsertBlockAfter(adjacent_, block_); - } - } else { - rational adjacent_length = adjacent_->length() - trim_diff_; - - if (mode_ == Timeline::kTrimIn) { - adjacent_->set_length_and_media_out(adjacent_length); - } else { - adjacent_->set_length_and_media_in(adjacent_length); - } - } - } - } - - 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_); - } - - if (dynamic_cast(block_)) { - // Whole transition needs to be invalidated - invalidate_range = block_->range(); - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); -} - -void BlockTrimCommand::prep() -{ - // Store old length - old_length_ = block_->length(); - - // Determine if the length isn't changing, in which case we set a flag to do nothing - if ((doing_nothing_ = (old_length_ == new_length_))) { - return; - } - - // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer - trim_diff_ = old_length_ - new_length_; - - // Retrieve our adjacent block (or nullptr if none) - if (mode_ == Timeline::kTrimIn) { - adjacent_ = block_->previous(); - } else { - adjacent_ = block_->next(); - } - - // Ignore when trimming the out with no adjacent, because the user must have trimmed the end - // of the last block in the track, so we don't need to do anything elses - needs_adjacent_ = (mode_ == Timeline::kTrimIn || adjacent_); - - if (needs_adjacent_) { - // If we're trimming shorter, we need an adjacent, so check if we have a viable one. - we_created_adjacent_ = (trim_diff_ > 0 && (!adjacent_ || (!dynamic_cast(adjacent_) && !trim_is_a_roll_edit_))); - - if (we_created_adjacent_) { - // We shortened but don't have a viable adjacent to lengthen, so we create one - adjacent_ = new GapBlock(); - adjacent_->set_length_and_media_out(trim_diff_); - } else { - // Determine if we're removing the adjacent - rational adjacent_length = adjacent_->length() + trim_diff_; - we_removed_adjacent_ = adjacent_length.isNull(); - } - } -} - -void TrackReplaceBlockWithGapCommand::redo() -{ - // Determine if this block is connected to any transitions that should also be removed by this operation - if (transition_remove_commands_.isEmpty()) { - CreateRemoveTransitionCommandIfNecessary(false); - CreateRemoveTransitionCommandIfNecessary(true); - } - for (auto it=transition_remove_commands_.cbegin(); it!=transition_remove_commands_.cend(); it++) { - (*it)->redo(); - } - - if (block_->next()) { - track_->BeginOperation(); - - // Invalidate the range inhabited by this block - TimeRange invalidate_range(block_->in(), block_->out()); - - // Block has a next, which means it's NOT at the end of the sequence and thus requires a gap - rational new_gap_length = block_->length(); - - Block* previous = block_->previous(); - Block* next = block_->next(); - - bool previous_is_a_gap = dynamic_cast(previous); - bool next_is_a_gap = dynamic_cast(next); - - if (previous_is_a_gap && next_is_a_gap) { - // Clip is preceded and followed by a gap, so we'll merge the two - existing_gap_ = static_cast(previous); - - existing_merged_gap_ = static_cast(next); - new_gap_length += existing_merged_gap_->length(); - track_->RippleRemoveBlock(existing_merged_gap_); - existing_merged_gap_->setParent(&memory_manager_); - } else if (previous_is_a_gap) { - // Extend this gap to fill space left by block - existing_gap_ = static_cast(previous); - } else if (next_is_a_gap) { - // Extend this gap to fill space left by block - existing_gap_ = static_cast(next); - } - - if (existing_gap_) { - // Extend an existing gap - new_gap_length += existing_gap_->length(); - existing_gap_->set_length_and_media_out(new_gap_length); - track_->RippleRemoveBlock(block_); - - existing_gap_precedes_ = (existing_gap_ == previous); - } else { - // No gap exists to fill this space, create a new one and swap it in - if (!our_gap_) { - our_gap_ = new GapBlock(); - our_gap_->set_length_and_media_out(new_gap_length); - } - - our_gap_->setParent(track_->parent()); - track_->ReplaceBlock(block_, our_gap_); - - if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, track_, our_gap_->index(), track_->Blocks().size(), true); - } - position_command_->redo(); - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); - - } else { - // Block is at the end of the track, simply remove it - Block* preceding = block_->previous(); - track_->RippleRemoveBlock(block_); - - // Determine if it's preceded by a gap, and remove that gap if so - if (dynamic_cast(preceding)) { - track_->RippleRemoveBlock(preceding); - preceding->setParent(&memory_manager_); - - existing_merged_gap_ = static_cast(preceding); - } - } -} - -void TrackReplaceBlockWithGapCommand::undo() -{ - if (our_gap_ || existing_gap_) { - track_->BeginOperation(); - - if (our_gap_) { - - // We made this gap, simply swap our gap back - track_->ReplaceBlock(our_gap_, block_); - our_gap_->setParent(&memory_manager_); - - position_command_->undo(); - - } else { - - // If we're here, assume that we extended an existing gap - rational original_gap_length = existing_gap_->length() - block_->length(); - - // If we merged two gaps together, restore the second one now - if (existing_merged_gap_) { - original_gap_length -= existing_merged_gap_->length(); - existing_merged_gap_->setParent(track_->parent()); - track_->InsertBlockAfter(existing_merged_gap_, existing_gap_); - existing_merged_gap_ = nullptr; - } - - // Restore original block - if (existing_gap_precedes_) { - track_->InsertBlockAfter(block_, existing_gap_); - } else { - track_->InsertBlockBefore(block_, existing_gap_); - } - - // Restore gap's original length - existing_gap_->set_length_and_media_out(original_gap_length); - - existing_gap_ = nullptr; - - } - - track_->EndOperation(); - - track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); - } else { - - // Our gap and existing gap were both null, our block must have been at the end and thus - // required no gap extension/replacement - - // However, we may have removed an unnecessary gap that preceded it - if (existing_merged_gap_) { - existing_merged_gap_->setParent(track_->parent()); - track_->AppendBlock(existing_merged_gap_); - existing_merged_gap_ = nullptr; - } - - // Restore block - track_->AppendBlock(block_); - - } - - for (auto it=transition_remove_commands_.crbegin(); it!=transition_remove_commands_.crend(); it++) { - (*it)->undo(); - } -} - -void TrackReplaceBlockWithGapCommand::CreateRemoveTransitionCommandIfNecessary(bool next) -{ - Block* relevant_block; - - if (next) { - relevant_block = block_->next(); - } else { - relevant_block = block_->previous(); - } - - TransitionBlock* transition_cast_test = dynamic_cast(relevant_block); - - if (transition_cast_test) { - if ((next && transition_cast_test->connected_out_block() == block_ && !transition_cast_test->connected_in_block()) - || (!next && transition_cast_test->connected_in_block() == block_ && !transition_cast_test->connected_out_block())) { - TransitionRemoveCommand* command = new TransitionRemoveCommand(transition_cast_test, true); - transition_remove_commands_.append(command); - } - } -} - -} diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h deleted file mode 100644 index 2bfeea16a..000000000 --- a/app/widget/timelinewidget/timelineundo.h +++ /dev/null @@ -1,2228 +0,0 @@ -/*** - - 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, nullptr, 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), - range_(in, 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, range_); - 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(range_.out(), range_.in()); - } else if (list_->type() == Track::kAudio) { - list_->parent()->ShiftAudioCache(range_.out(), range_.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(range_.in(), range_.out()); - } else if (list_->type() == Track::kAudio) { - list_->parent()->ShiftAudioCache(range_.in(), range_.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(); - track->Node::InvalidateCache(range_, Track::kBlockInput); - } - } - } - -private: - TrackList* list_; - - QList working_tracks_; - - TimeRange range_; - - bool all_tracks_unlocked_; - - QVector commands_; - -}; - -class TimelineRippleRemoveAreaCommand : public MultiUndoCommand { -public: - TimelineRippleRemoveAreaCommand(Sequence* timeline, rational in, rational out) : - timeline_(timeline) - { - for (int i=0; itrack_list(static_cast(i)), - in, - out)); - } - } - - virtual Project* GetRelevantProject() const override - { - return timeline_->project(); - } - -private: - Sequence* timeline_; - -}; - -class TrackListRippleToolCommand : public UndoCommand { -public: - struct RippleInfo { - Block* block; - bool append_gap; - }; - - 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) - { - all_tracks_unlocked_ = (info_.size() == track_list_->GetTrackCount()); - } - - virtual Project* GetRelevantProject() const override - { - return track_list_->parent()->project(); - } - - virtual void redo() override - { - ripple(true); - } - - virtual void undo() override - { - ripple(false); - } - -private: - void 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; - - // Begin operation so we can invalidate better later - track->BeginOperation(); - - 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); - } - - if (all_tracks_unlocked_) { - // We rippled all the tracks, so we can shift the whole cache - if (track_list_->type() == Track::kVideo) { - track_list_->parent()->ShiftVideoCache(pre_latest_out, post_latest_out); - } else if (track_list_->type() == Track::kAudio) { - track_list_->parent()->ShiftAudioCache(pre_latest_out, post_latest_out); - } - } - - for (auto it=working_data_.cbegin(); it!=working_data_.cend(); it++) { - Track* track = it.key(); - - track->EndOperation(); - - if (!all_tracks_unlocked_) { - // If we're not shifting, the whole track must get invalidated - track->Node::InvalidateCache(TimeRange(it.value().earliest_point_of_change, RATIONAL_MAX), Track::kBlockInput); - } - } - } - - TrackList* track_list_; - - QHash info_; - rational ripple_movement_; - Timeline::MovementMode movement_mode_; - - struct WorkingData { - GapBlock* created_gap = nullptr; - Block* removed_gap_after; - rational old_length; - rational earliest_point_of_change; - }; - - QHash working_data_; - - QObject memory_manager_; - - bool all_tracks_unlocked_; - -}; - -class TimelineAddTrackCommand : public UndoCommand { -public: - TimelineAddTrackCommand(TrackList *timeline) : - TimelineAddTrackCommand(timeline, Config::Current()[QStringLiteral("AutoMergeTracks")].toBool()) - { - } - - 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_); - } - } - - static Track* RunImmediately(TrackList *timeline) - { - TimelineAddTrackCommand c(timeline); - c.redo(); - return c.track(); - } - - static Track* RunImmediately(TrackList *timeline, bool automerge) - { - TimelineAddTrackCommand c(timeline, automerge); - c.redo(); - return c.track(); - } - - virtual ~TimelineAddTrackCommand() override - { - delete position_command_; - } - - Track* track() const - { - return track_; - } - - virtual Project* GetRelevantProject() const override - { - return timeline_->parent()->project(); - } - - virtual void redo() override - { - // 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(); - } - - virtual void undo() override - { - position_command_->undo(); - - // 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_); - } - -private: - TrackList* timeline_; - - Track* track_; - Node* merge_; - NodeInput base_; - NodeInput blend_; - - NodeInput direct_; - - MultiUndoCommand* position_command_; - - QObject memory_manager_; - -}; - -/** - * @brief Destructively places `block` at the in point `start` - * - * The Block is guaranteed to be placed at the starting point specified. If there are Blocks in this area, they are - * either trimmed or removed to make space for this Block. Additionally, if the Block is placed beyond the end of - * the Sequence, a GapBlock is inserted to compensate. - */ -class TrackPlaceBlockCommand : public UndoCommand { -public: - TrackPlaceBlockCommand(TrackList *timeline, int track, Block* block, rational in) : - timeline_(timeline), - track_index_(track), - in_(in), - gap_(nullptr), - insert_(block), - ripple_remove_command_(nullptr) - { - } - - virtual ~TrackPlaceBlockCommand() override - { - delete ripple_remove_command_; - qDeleteAll(add_track_commands_); - qDeleteAll(position_commands_); - } - - virtual Project* GetRelevantProject() const override - { - return timeline_->parent()->project(); - } - - virtual void redo() override - { - TimeRangeList ranges_to_invalidate; - - // Determine if we need to add tracks - if (track_index_ >= timeline_->GetTracks().size()) { - if (add_track_commands_.isEmpty()) { - // First redo, create tracks now - add_track_commands_.resize(track_index_ - timeline_->GetTracks().size() + 1); - - for (int i=0; iredo(); - } - } - - Track* track = timeline_->GetTrackAt(track_index_); - - track->BeginOperation(); - - bool 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 - if (!gap_) { - gap_ = new GapBlock(); - gap_->set_length_and_media_out(in_ - track->track_length()); - } - gap_->setParent(track->parent()); - track->AppendBlock(gap_); - ranges_to_invalidate.insert(gap_->range()); - } - - track->AppendBlock(insert_); - - if (position_commands_.isEmpty()) { - // Create position commands for insert and gap if necessary - if (gap_) { - position_commands_.append(new NodeSetPositionAsChildCommand(gap_, track, track, gap_->index(), track->Blocks().size(), true)); - } - position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); - } - } else { - // Place the Block at this point - if (!ripple_remove_command_) { - ripple_remove_command_ = new TrackRippleRemoveAreaCommand(track, - TimeRange(in_, in_ + insert_->length())); - - } - - ripple_remove_command_->redo(); - track->InsertBlockAfter(insert_, ripple_remove_command_->GetInsertionIndex()); - - if (position_commands_.isEmpty()) { - position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); - } - } - - track->EndOperation(); - - ranges_to_invalidate.insert(insert_->range()); - - foreach (const TimeRange &r, ranges_to_invalidate) { - track->Node::InvalidateCache(r, Track::kBlockInput); - } - - for (int i=0; iredo(); - } - } - - virtual void undo() override - { - for (int i=position_commands_.size()-1; i>=0; i--) { - position_commands_.at(i)->undo(); - } - - Track* t = timeline_->GetTrackAt(track_index_); - - TimeRange insert_range(insert_->in(), insert_->out()); - - // Firstly, remove our insert - t->BeginOperation(); - t->RippleRemoveBlock(insert_); - - if (ripple_remove_command_) { - // If we ripple removed, just undo that - ripple_remove_command_->undo(); - } else if (gap_) { - t->RippleRemoveBlock(gap_); - gap_->setParent(&memory_manager_); - } - t->EndOperation(); - - if (ripple_remove_command_) { - t->Node::InvalidateCache(insert_range, Track::kBlockInput); - } - - // Remove tracks if we added them - for (int i=add_track_commands_.size()-1; i>=0; i--) { - add_track_commands_.at(i)->undo(); - } - } - -private: - TrackList* timeline_; - int track_index_; - rational in_; - GapBlock* gap_; - Block* insert_; - QVector add_track_commands_; - QObject memory_manager_; - TrackRippleRemoveAreaCommand* ripple_remove_command_; - QVector position_commands_; - -}; - -/** - * @brief Replaces Block `old` with Block `replace` - * - * Both blocks must have equal lengths. - */ -class TrackReplaceBlockCommand : public UndoCommand { -public: - TrackReplaceBlockCommand(Track* track, Block* old, Block* replace) : - track_(track), - old_(old), - replace_(replace) - { - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - track_->ReplaceBlock(old_, replace_); - } - - virtual void undo() override - { - track_->ReplaceBlock(replace_, old_); - } - -private: - Track* track_; - Block* old_; - Block* replace_; - -}; - -class TransitionRemoveCommand : public UndoCommand { -public: - TransitionRemoveCommand(TransitionBlock* block, bool remove_from_graph) : - block_(block), - remove_from_graph_(remove_from_graph), - remove_command_(nullptr) - { - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - 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(); - } - } - - virtual void undo() override - { - if (remove_from_graph_) { - remove_command_->undo(); - } - - 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); - } - -private: - TransitionBlock* block_; - - Track* track_; - - Block* out_block_; - Block* in_block_; - - bool remove_from_graph_; - UndoCommand* remove_command_; - -}; - -class TrackReplaceBlockWithGapCommand : public UndoCommand { -public: - TrackReplaceBlockWithGapCommand(Track* track, Block* block) : - track_(track), - block_(block), - existing_gap_(nullptr), - existing_merged_gap_(nullptr), - our_gap_(nullptr), - position_command_(nullptr) - { - } - - virtual ~TrackReplaceBlockWithGapCommand() override - { - delete position_command_; - } - - virtual Project* GetRelevantProject() const override - { - return block_->project(); - } - - virtual void redo() override; - - virtual void undo() override; - -private: - void CreateRemoveTransitionCommandIfNecessary(bool next); - - Track* track_; - Block* block_; - - GapBlock* existing_gap_; - GapBlock* existing_merged_gap_; - bool existing_gap_precedes_; - GapBlock* our_gap_; - - NodeSetPositionAsChildCommand* position_command_; - - QObject memory_manager_; - - QVector transition_remove_commands_; - -}; - -class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { -public: - TimelineRippleDeleteGapsAtRegionsCommand(Sequence* vo, const TimeRangeList& regions) : - timeline_(vo), - regions_(regions) - { - } - - virtual ~TimelineRippleDeleteGapsAtRegionsCommand() override - { - qDeleteAll(commands_); - } - - virtual Project* GetRelevantProject() const override - { - return timeline_->project(); - } - - virtual void redo() override - { - if (commands_.isEmpty()) { - foreach (const TimeRange& range, regions_) { - rational max_ripple_length = range.length(); - - QVector blocks_around_range; - - foreach (Track* 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 (dynamic_cast(block_at_time)) { - 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 - commands_.append(new TrackRippleRemoveBlockCommand(resize->track(), resize)); - } else { - // Resize block - commands_.append(new BlockResizeCommand(resize, resize->length() - max_ripple_length)); - } - } - } - } - } - - foreach (UndoCommand* c, commands_) { - c->redo(); - } - } - - virtual void undo() override - { - for (int i=commands_.size()-1;i>=0;i--) { - commands_.at(i)->undo(); - } - } - -private: - Sequence* timeline_; - TimeRangeList regions_; - - QVector commands_; - -}; - -class WorkareaSetEnabledCommand : public UndoCommand { -public: - WorkareaSetEnabledCommand(Project *project, TimelinePoints* points, bool enabled) : - project_(project), - points_(points), - old_enabled_(points_->workarea()->enabled()), - new_enabled_(enabled) - { - } - - virtual Project* GetRelevantProject() const override - { - return project_; - } - - virtual void redo() override - { - points_->workarea()->set_enabled(new_enabled_); - } - - virtual void undo() override - { - points_->workarea()->set_enabled(old_enabled_); - } - -private: - Project* project_; - - TimelinePoints* points_; - - bool old_enabled_; - - bool new_enabled_; - -}; - -class WorkareaSetRangeCommand : public UndoCommand { -public: - WorkareaSetRangeCommand(Project *project, TimelinePoints* points, const TimeRange& range) : - project_(project), - points_(points), - old_range_(points_->workarea()->range()), - new_range_(range) - { - } - - virtual Project* GetRelevantProject() const override - { - return project_; - } - - virtual void redo() override - { - points_->workarea()->set_range(new_range_); - } - - virtual void undo() override - { - points_->workarea()->set_range(old_range_); - } - -private: - Project* project_; - - TimelinePoints* points_; - - TimeRange old_range_; - - TimeRange new_range_; - -}; - -class BlockEnableDisableCommand : public UndoCommand { -public: - BlockEnableDisableCommand(Block* block, bool enabled) : - block_(block), - old_enabled_(block_->is_enabled()), - new_enabled_(enabled) - { - } - - virtual Project* GetRelevantProject() const override - { - return block_->project(); - } - - virtual void redo() override - { - block_->set_enabled(new_enabled_); - } - - virtual void undo() override - { - block_->set_enabled(old_enabled_); - } - -private: - Block* block_; - - bool old_enabled_; - - bool new_enabled_; - -}; - -class TrackSlideCommand : public UndoCommand { -public: - TrackSlideCommand(Track* track, const QList& moving_blocks, Block* in_adjacent, Block* out_adjacent, const rational& movement) : - prepped_(false), - track_(track), - blocks_(moving_blocks), - movement_(movement), - in_adjacent_(in_adjacent), - in_adjacent_remove_command_(nullptr), - out_adjacent_(out_adjacent), - out_adjacent_remove_command_(nullptr) - { - Q_ASSERT(!movement_.isNull()); - } - - virtual ~TrackSlideCommand() override - { - delete in_adjacent_remove_command_; - delete out_adjacent_remove_command_; - } - - virtual Project* GetRelevantProject() const override - { - return track_->project(); - } - - virtual void redo() override - { - if (!prepped_) { - prep(); - prepped_ = true; - } - - // Make sure all movement blocks' old positions are invalidated - TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); - - track_->BeginOperation(); - - // We will always have an in adjacent if there was a valid slide - if (we_created_in_adjacent_) { - // We created in adjacent, so all we have to do is insert it - in_adjacent_->setParent(track_->parent()); - track_->InsertBlockBefore(in_adjacent_, blocks_.first()); - } else if (-movement_ == in_adjacent_->length()) { - // Movement will remove in adjacent - track_->RippleRemoveBlock(in_adjacent_); - - if (NodeCanBeRemoved(in_adjacent_)) { - if (!in_adjacent_remove_command_) { - in_adjacent_remove_command_ = CreateRemoveCommand(in_adjacent_); - } - - in_adjacent_remove_command_->redo(); - } - } else { - // Simply resize adjacent - in_adjacent_->set_length_and_media_out(in_adjacent_->length() + movement_); - } - - // We may not have an out adjacent if the slide was at the end of the track - if (out_adjacent_) { - if (we_created_out_adjacent_) { - // We created out adjacent, so we just have to insert it - out_adjacent_->setParent(track_->parent()); - track_->InsertBlockAfter(out_adjacent_, blocks_.last()); - } else if (movement_ == out_adjacent_->length()) { - // Movement will remove out adjacent - track_->RippleRemoveBlock(out_adjacent_); - - if (NodeCanBeRemoved(out_adjacent_)) { - if (!out_adjacent_remove_command_) { - out_adjacent_remove_command_ = CreateRemoveCommand(out_adjacent_); - } - - out_adjacent_remove_command_->redo(); - } - } else { - // Simply resize adjacent - out_adjacent_->set_length_and_media_in(out_adjacent_->length() - movement_); - } - } - - track_->EndOperation(); - - // Make sure all movement blocks' new positions are invalidated - invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), - qMax(invalidate_range.out(), blocks_.last()->out())); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); - } - - virtual void undo() override - { - // Make sure all movement blocks' old positions are invalidated - TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); - - track_->BeginOperation(); - - if (we_created_in_adjacent_) { - // We created this, so we can remove it now - track_->RippleRemoveBlock(in_adjacent_); - in_adjacent_->setParent(&memory_manager_); - } else if (in_adjacent_remove_command_) { - // We removed this, so we can restore it now - in_adjacent_remove_command_->undo(); - } else { - // Simply resize adjacent - in_adjacent_->set_length_and_media_out(in_adjacent_->length() - movement_); - } - - if (out_adjacent_) { - if (we_created_out_adjacent_) { - // We created this, so we can remove it now - track_->RippleRemoveBlock(out_adjacent_); - out_adjacent_->setParent(&memory_manager_); - } else if (out_adjacent_remove_command_) { - out_adjacent_remove_command_->undo(); - } else { - out_adjacent_->set_length_and_media_in(out_adjacent_->length() + movement_); - } - } - - track_->EndOperation(); - - // Make sure all movement blocks' new positions are invalidated - invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), - qMax(invalidate_range.out(), blocks_.last()->out())); - - track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); - } - -private: - bool prepped_; - - void prep() - { - if (!in_adjacent_) { - in_adjacent_ = new GapBlock(); - in_adjacent_->set_length_and_media_out(movement_); - in_adjacent_->setParent(&memory_manager_); - we_created_in_adjacent_ = true; - } else { - we_created_in_adjacent_ = false; - } - - if (!out_adjacent_ && blocks_.last()->next()) { - out_adjacent_ = new GapBlock(); - out_adjacent_->set_length_and_media_out(-movement_); - out_adjacent_->setParent(&memory_manager_); - we_created_out_adjacent_ = true; - } else { - we_created_out_adjacent_ = false; - } - } - - Track* track_; - QList blocks_; - rational movement_; - - bool we_created_in_adjacent_; - Block* in_adjacent_; - UndoCommand* in_adjacent_remove_command_; - bool we_created_out_adjacent_; - Block* out_adjacent_; - UndoCommand* out_adjacent_remove_command_; - - QObject memory_manager_; - -}; - -class TrackListInsertGaps : public UndoCommand { -public: - TrackListInsertGaps(TrackList* track_list, const rational& point, const rational& length) : - prepped_(false), - track_list_(track_list), - point_(point), - length_(length), - split_command_(nullptr) - { - } - - virtual ~TrackListInsertGaps() override - { - delete split_command_; - } - - virtual Project* GetRelevantProject() const override - { - return track_list_->parent()->project(); - } - - virtual void redo() override - { - if (!prepped_) { - prep(); - prepped_ = true; - } - - if (all_tracks_unlocked_) { - // Optimize by shifting over since we have a constant amount of time being inserted - if (track_list_->type() == Track::kVideo) { - track_list_->parent()->ShiftVideoCache(point_, point_ + length_); - } else if (track_list_->type() == Track::kAudio) { - track_list_->parent()->ShiftAudioCache(point_, point_ + length_); - } - } - - foreach (Track* track, working_tracks_) { - track->BeginOperation(); - } - - foreach (Block* gap, gaps_to_extend_) { - gap->set_length_and_media_out(gap->length() + length_); - } - - if (split_command_) { - split_command_->redo(); - } - - foreach (auto add_gap, gaps_added_) { - add_gap.gap->setParent(add_gap.track->parent()); - add_gap.track->InsertBlockAfter(add_gap.gap, add_gap.before); - } - - foreach (Track* track, working_tracks_) { - track->EndOperation(); - } - - if (!all_tracks_unlocked_) { - foreach (Track* track, working_tracks_) { - track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); - } - } - } - - virtual void undo() override - { - if (all_tracks_unlocked_) { - // Optimize by shifting over since we have a constant amount of time being inserted - if (track_list_->type() == Track::kVideo) { - track_list_->parent()->ShiftVideoCache(point_ + length_, point_); - } else if (track_list_->type() == Track::kAudio) { - track_list_->parent()->ShiftAudioCache(point_ + length_, point_); - } - } - - foreach (Track* track, working_tracks_) { - track->BeginOperation(); - } - - // Remove added gaps - foreach (auto add_gap, gaps_added_) { - add_gap.gap->track()->RippleRemoveBlock(add_gap.gap); - add_gap.gap->setParent(&memory_manager_); - } - - // Un-split blocks - if (split_command_) { - split_command_->undo(); - } - - // Restore original length of gaps - foreach (Block* gap, gaps_to_extend_) { - gap->set_length_and_media_out(gap->length() - length_); - } - - foreach (Track* track, working_tracks_) { - track->EndOperation(); - } - - if (!all_tracks_unlocked_) { - foreach (Track* track, working_tracks_) { - track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); - } - } - } - -private: - bool prepped_; - - void prep() - { - // 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 tracks_to_append_gap_to; - - foreach (Track* track, working_tracks_) { - foreach (Block* b, track->Blocks()) { - if (dynamic_cast(b) && b->in() <= point_ && b->out() >= point_) { - // Found a gap at the location - gaps_to_extend_.append(b); - break; - } else if (dynamic_cast(b) && b->out() >= point_) { - bool append_gap = true; - - if (b->in() == point_) { - // The only reason we should be here is if this block is at the start of the track, - // in which case no split needs to occur - b = nullptr; - } else if (b->out() > point_) { - // Block must be split as well as having a gap appended to it - blocks_to_split.append(b); - } else if (!b->next()) { - // At the end of a track, no gap needs to be added at all - append_gap = false; - } - - if (append_gap) { - tracks_to_append_gap_to.append(track); - blocks_to_append_gap_to.append(b); - } - break; - } - } - } - - if (!blocks_to_split.isEmpty()) { - split_command_ = new BlockSplitPreservingLinksCommand(blocks_to_split, {point_}); - } - - for (int i=0; iset_length_and_media_out(length_); - gap->setParent(&memory_manager_); - gaps_added_.append({gap, blocks_to_append_gap_to.at(i), tracks_to_append_gap_to.at(i)}); - } - } - - TrackList* track_list_; - - rational point_; - - rational length_; - - QVector working_tracks_; - - bool all_tracks_unlocked_; - - QVector gaps_to_extend_; - - struct AddGap { - GapBlock* gap; - Block* before; - Track* track; - }; - - QVector gaps_added_; - - BlockSplitPreservingLinksCommand* split_command_; - - QObject memory_manager_; - -}; - -} - -#endif // TIMELINEUNDOABLE_H diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 63fda18a4..8b973b31c 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -44,6 +44,10 @@ #include "tool/zoom.h" #include "tool/tool.h" #include "trackview/trackview.h" +#include "undo/timelineundogeneral.h" +#include "undo/timelineundopointer.h" +#include "undo/timelineundoripple.h" +#include "undo/timelineundoworkarea.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" #include "widget/nodeview/nodeviewundo.h" diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index fd0011bc1..9a1273bed 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -25,6 +25,7 @@ #include "node/generator/solid/solid.h" #include "node/generator/text/text.h" #include "widget/timelinewidget/timelinewidget.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 137bbdc06..ff9ee2629 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -35,6 +35,7 @@ #include "node/math/math/math.h" #include "node/project/sequence/sequence.h" #include "widget/nodeview/nodeviewundo.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" #include "window/mainwindow/mainwindow.h" #include "window/mainwindow/mainwindowundo.h" diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 3d2ffee40..dade2c454 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -34,6 +34,7 @@ #include "node/block/transition/transition.h" #include "pointer.h" #include "widget/nodeview/nodeviewundo.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/razor.cpp b/app/widget/timelinewidget/tool/razor.cpp index fadc7b7c0..ae461da2b 100644 --- a/app/widget/timelinewidget/tool/razor.cpp +++ b/app/widget/timelinewidget/tool/razor.cpp @@ -20,6 +20,7 @@ #include "razor.h" #include "widget/timelinewidget/timelinewidget.h" +#include "widget/timelinewidget/undo/timelineundosplit.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index ba5e3dce9..3d0861a28 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -23,6 +23,7 @@ #include "node/block/gap/gap.h" #include "ripple.h" #include "widget/nodeview/nodeviewundo.h" +#include "widget/timelinewidget/undo/timelineundoripple.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/slip.cpp b/app/widget/timelinewidget/tool/slip.cpp index 21d3db970..e7f66881f 100644 --- a/app/widget/timelinewidget/tool/slip.cpp +++ b/app/widget/timelinewidget/tool/slip.cpp @@ -25,6 +25,7 @@ #include "common/timecodefunctions.h" #include "config/config.h" #include "slip.h" +#include "widget/timelinewidget/undo/timelineundogeneral.h" namespace olive { diff --git a/app/widget/timelinewidget/tool/tool.h b/app/widget/timelinewidget/tool/tool.h index b898d1c48..d229e1ad3 100644 --- a/app/widget/timelinewidget/tool/tool.h +++ b/app/widget/timelinewidget/tool/tool.h @@ -25,7 +25,6 @@ #include "common/rational.h" #include "widget/nodeview/nodeviewundo.h" -#include "widget/timelinewidget/timelineundo.h" #include "widget/timelinewidget/view/timelineviewghostitem.h" #include "widget/timelinewidget/view/timelineviewmouseevent.h" diff --git a/app/widget/timelinewidget/tool/transition.cpp b/app/widget/timelinewidget/tool/transition.cpp index b66ba7394..cd9c72717 100644 --- a/app/widget/timelinewidget/tool/transition.cpp +++ b/app/widget/timelinewidget/tool/transition.cpp @@ -25,7 +25,7 @@ #include "node/factory.h" #include "transition.h" #include "widget/nodeview/nodeviewundo.h" -#include "widget/timelinewidget/timelineundo.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" namespace olive { diff --git a/app/widget/timelinewidget/undo/CMakeLists.txt b/app/widget/timelinewidget/undo/CMakeLists.txt new file mode 100644 index 000000000..a3b72c454 --- /dev/null +++ b/app/widget/timelinewidget/undo/CMakeLists.txt @@ -0,0 +1,33 @@ +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/timelinewidget/undo/timelineundocommon.h + widget/timelinewidget/undo/timelineundogeneral.cpp + widget/timelinewidget/undo/timelineundogeneral.h + widget/timelinewidget/undo/timelineundopointer.cpp + widget/timelinewidget/undo/timelineundopointer.h + widget/timelinewidget/undo/timelineundoripple.cpp + widget/timelinewidget/undo/timelineundoripple.h + widget/timelinewidget/undo/timelineundosplit.cpp + widget/timelinewidget/undo/timelineundosplit.h + widget/timelinewidget/undo/timelineundotrack.cpp + widget/timelinewidget/undo/timelineundotrack.h + widget/timelinewidget/undo/timelineundoworkarea.cpp + widget/timelinewidget/undo/timelineundoworkarea.h + PARENT_SCOPE +) diff --git a/app/widget/timelinewidget/undo/timelineundocommon.h b/app/widget/timelinewidget/undo/timelineundocommon.h new file mode 100644 index 000000000..d25de0679 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundocommon.h @@ -0,0 +1,48 @@ +/*** + + 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 TIMELINEUNDOCOMMON_H +#define TIMELINEUNDOCOMMON_H + +#include "node/node.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; +} + +} + +#endif // TIMELINEUNDOCOMMON_H diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.cpp b/app/widget/timelinewidget/undo/timelineundogeneral.cpp new file mode 100644 index 000000000..6deb5586d --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundogeneral.cpp @@ -0,0 +1,611 @@ +/*** + + 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(); +} + +void TimelineAddTrackCommand::undo() +{ + position_command_->undo(); + + // 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(); + } +} + +void TransitionRemoveCommand::undo() +{ + if (remove_from_graph_) { + remove_command_->undo(); + } + + 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 tracks_to_append_gap_to; + + foreach (Track* track, working_tracks_) { + foreach (Block* b, track->Blocks()) { + if (dynamic_cast(b) && b->in() <= point_ && b->out() >= point_) { + // Found a gap at the location + gaps_to_extend_.append(b); + break; + } else if (dynamic_cast(b) && b->out() >= point_) { + bool append_gap = true; + + if (b->in() == point_) { + // The only reason we should be here is if this block is at the start of the track, + // in which case no split needs to occur + b = nullptr; + } else if (b->out() > point_) { + // Block must be split as well as having a gap appended to it + blocks_to_split.append(b); + } else if (!b->next()) { + // At the end of a track, no gap needs to be added at all + append_gap = false; + } + + if (append_gap) { + tracks_to_append_gap_to.append(track); + blocks_to_append_gap_to.append(b); + } + break; + } + } + } + + if (!blocks_to_split.isEmpty()) { + split_command_ = new BlockSplitPreservingLinksCommand(blocks_to_split, {point_}); + } + + for (int i=0; iset_length_and_media_out(length_); + gap->setParent(&memory_manager_); + gaps_added_.append({gap, blocks_to_append_gap_to.at(i), tracks_to_append_gap_to.at(i)}); + } +} + +void TrackListInsertGaps::redo() +{ + if (all_tracks_unlocked_) { + // Optimize by shifting over since we have a constant amount of time being inserted + if (track_list_->type() == Track::kVideo) { + track_list_->parent()->ShiftVideoCache(point_, point_ + length_); + } else if (track_list_->type() == Track::kAudio) { + track_list_->parent()->ShiftAudioCache(point_, point_ + length_); + } + } + + foreach (Track* track, working_tracks_) { + track->BeginOperation(); + } + + foreach (Block* gap, gaps_to_extend_) { + gap->set_length_and_media_out(gap->length() + length_); + } + + if (split_command_) { + split_command_->redo(); + } + + foreach (auto add_gap, gaps_added_) { + add_gap.gap->setParent(add_gap.track->parent()); + add_gap.track->InsertBlockAfter(add_gap.gap, add_gap.before); + } + + foreach (Track* track, working_tracks_) { + track->EndOperation(); + } + + if (!all_tracks_unlocked_) { + foreach (Track* track, working_tracks_) { + track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); + } + } +} + +void TrackListInsertGaps::undo() +{ + if (all_tracks_unlocked_) { + // Optimize by shifting over since we have a constant amount of time being inserted + if (track_list_->type() == Track::kVideo) { + track_list_->parent()->ShiftVideoCache(point_ + length_, point_); + } else if (track_list_->type() == Track::kAudio) { + track_list_->parent()->ShiftAudioCache(point_ + length_, point_); + } + } + + foreach (Track* track, working_tracks_) { + track->BeginOperation(); + } + + // Remove added gaps + foreach (auto add_gap, gaps_added_) { + add_gap.gap->track()->RippleRemoveBlock(add_gap.gap); + add_gap.gap->setParent(&memory_manager_); + } + + // Un-split blocks + if (split_command_) { + split_command_->undo(); + } + + // Restore original length of gaps + foreach (Block* gap, gaps_to_extend_) { + gap->set_length_and_media_out(gap->length() - length_); + } + + foreach (Track* track, working_tracks_) { + track->EndOperation(); + } + + if (!all_tracks_unlocked_) { + foreach (Track* track, working_tracks_) { + track->Node::InvalidateCache(TimeRange(point_, RATIONAL_MAX), Track::kBlockInput); + } + } +} + +// +// TrackReplaceBlockWithGapCommand +// +void TrackReplaceBlockWithGapCommand::redo() +{ + // Determine if this block is connected to any transitions that should also be removed by this operation + if (transition_remove_commands_.isEmpty()) { + CreateRemoveTransitionCommandIfNecessary(false); + CreateRemoveTransitionCommandIfNecessary(true); + } + for (auto it=transition_remove_commands_.cbegin(); it!=transition_remove_commands_.cend(); it++) { + (*it)->redo(); + } + + if (block_->next()) { + track_->BeginOperation(); + + // Invalidate the range inhabited by this block + TimeRange invalidate_range(block_->in(), block_->out()); + + // Block has a next, which means it's NOT at the end of the sequence and thus requires a gap + rational new_gap_length = block_->length(); + + Block* previous = block_->previous(); + Block* next = block_->next(); + + bool previous_is_a_gap = dynamic_cast(previous); + bool next_is_a_gap = dynamic_cast(next); + + if (previous_is_a_gap && next_is_a_gap) { + // Clip is preceded and followed by a gap, so we'll merge the two + existing_gap_ = static_cast(previous); + + existing_merged_gap_ = static_cast(next); + new_gap_length += existing_merged_gap_->length(); + track_->RippleRemoveBlock(existing_merged_gap_); + existing_merged_gap_->setParent(&memory_manager_); + } else if (previous_is_a_gap) { + // Extend this gap to fill space left by block + existing_gap_ = static_cast(previous); + } else if (next_is_a_gap) { + // Extend this gap to fill space left by block + existing_gap_ = static_cast(next); + } + + if (existing_gap_) { + // Extend an existing gap + new_gap_length += existing_gap_->length(); + existing_gap_->set_length_and_media_out(new_gap_length); + track_->RippleRemoveBlock(block_); + + existing_gap_precedes_ = (existing_gap_ == previous); + } else { + // No gap exists to fill this space, create a new one and swap it in + if (!our_gap_) { + our_gap_ = new GapBlock(); + our_gap_->set_length_and_media_out(new_gap_length); + } + + our_gap_->setParent(track_->parent()); + track_->ReplaceBlock(block_, our_gap_); + + if (!position_command_) { + position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, track_, our_gap_->index(), track_->Blocks().size(), true); + } + position_command_->redo(); + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); + + } else { + // Block is at the end of the track, simply remove it + Block* preceding = block_->previous(); + track_->RippleRemoveBlock(block_); + + // Determine if it's preceded by a gap, and remove that gap if so + if (dynamic_cast(preceding)) { + track_->RippleRemoveBlock(preceding); + preceding->setParent(&memory_manager_); + + existing_merged_gap_ = static_cast(preceding); + } + } +} + +void TrackReplaceBlockWithGapCommand::undo() +{ + if (our_gap_ || existing_gap_) { + track_->BeginOperation(); + + if (our_gap_) { + + // We made this gap, simply swap our gap back + track_->ReplaceBlock(our_gap_, block_); + our_gap_->setParent(&memory_manager_); + + position_command_->undo(); + + } else { + + // If we're here, assume that we extended an existing gap + rational original_gap_length = existing_gap_->length() - block_->length(); + + // If we merged two gaps together, restore the second one now + if (existing_merged_gap_) { + original_gap_length -= existing_merged_gap_->length(); + existing_merged_gap_->setParent(track_->parent()); + track_->InsertBlockAfter(existing_merged_gap_, existing_gap_); + existing_merged_gap_ = nullptr; + } + + // Restore original block + if (existing_gap_precedes_) { + track_->InsertBlockAfter(block_, existing_gap_); + } else { + track_->InsertBlockBefore(block_, existing_gap_); + } + + // Restore gap's original length + existing_gap_->set_length_and_media_out(original_gap_length); + + existing_gap_ = nullptr; + + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); + } else { + + // Our gap and existing gap were both null, our block must have been at the end and thus + // required no gap extension/replacement + + // However, we may have removed an unnecessary gap that preceded it + if (existing_merged_gap_) { + existing_merged_gap_->setParent(track_->parent()); + track_->AppendBlock(existing_merged_gap_); + existing_merged_gap_ = nullptr; + } + + // Restore block + track_->AppendBlock(block_); + + } + + for (auto it=transition_remove_commands_.crbegin(); it!=transition_remove_commands_.crend(); it++) { + (*it)->undo(); + } +} + +void TrackReplaceBlockWithGapCommand::CreateRemoveTransitionCommandIfNecessary(bool next) +{ + Block* relevant_block; + + if (next) { + relevant_block = block_->next(); + } else { + relevant_block = block_->previous(); + } + + TransitionBlock* transition_cast_test = dynamic_cast(relevant_block); + + if (transition_cast_test) { + if ((next && transition_cast_test->connected_out_block() == block_ && !transition_cast_test->connected_in_block()) + || (!next && transition_cast_test->connected_in_block() == block_ && !transition_cast_test->connected_out_block())) { + TransitionRemoveCommand* command = new TransitionRemoveCommand(transition_cast_test, true); + transition_remove_commands_.append(command); + } + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.h b/app/widget/timelinewidget/undo/timelineundogeneral.h new file mode 100644 index 000000000..f1dd6075f --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundogeneral.h @@ -0,0 +1,325 @@ +/*** + + 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 TIMELINEUNDOGENERAL_H +#define TIMELINEUNDOGENERAL_H + +#include "config/config.h" +#include "node/block/gap/gap.h" +#include "node/output/track/track.h" +#include "node/output/track/tracklist.h" +#include "node/output/viewer/viewer.h" +#include "node/project/sequence/sequence.h" +#include "timelineundosplit.h" + +namespace olive { + +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; + virtual void undo() override; + +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 + { + return block_->project(); + } + + virtual void redo(); + virtual void undo(); + +private: + Block* block_; + rational old_length_; + rational new_length_; + +}; + +class BlockSetMediaInCommand : public UndoCommand { +public: + BlockSetMediaInCommand(Block* block, rational new_media_in) : + block_(block), + new_media_in_(new_media_in) + { + } + + virtual Project* GetRelevantProject() const + { + return block_->project(); + } + + virtual void redo(); + virtual void undo(); + +private: + Block* block_; + rational old_media_in_; + rational new_media_in_; + +}; + +class TimelineAddTrackCommand : public UndoCommand { +public: + TimelineAddTrackCommand(TrackList *timeline) : + TimelineAddTrackCommand(timeline, Config::Current()[QStringLiteral("AutoMergeTracks")].toBool()) + { + } + + TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks); + + static Track* RunImmediately(TrackList *timeline) + { + TimelineAddTrackCommand c(timeline); + c.redo(); + return c.track(); + } + + static Track* RunImmediately(TrackList *timeline, bool automerge) + { + TimelineAddTrackCommand c(timeline, automerge); + c.redo(); + return c.track(); + } + + virtual ~TimelineAddTrackCommand() override + { + delete position_command_; + } + + Track* track() const + { + return track_; + } + + virtual Project* GetRelevantProject() const override + { + return timeline_->parent()->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + TrackList* timeline_; + + Track* track_; + Node* merge_; + NodeInput base_; + NodeInput blend_; + + NodeInput direct_; + + MultiUndoCommand* position_command_; + + QObject memory_manager_; + +}; + +class TransitionRemoveCommand : public UndoCommand { +public: + TransitionRemoveCommand(TransitionBlock* block, bool remove_from_graph) : + block_(block), + remove_from_graph_(remove_from_graph), + remove_command_(nullptr) + { + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + TransitionBlock* block_; + + Track* track_; + + Block* out_block_; + Block* in_block_; + + bool remove_from_graph_; + UndoCommand* remove_command_; + +}; + +class TrackReplaceBlockWithGapCommand : public UndoCommand { +public: + TrackReplaceBlockWithGapCommand(Track* track, Block* block) : + track_(track), + block_(block), + existing_gap_(nullptr), + existing_merged_gap_(nullptr), + our_gap_(nullptr), + position_command_(nullptr) + { + } + + virtual ~TrackReplaceBlockWithGapCommand() override + { + delete position_command_; + } + + virtual Project* GetRelevantProject() const override + { + return block_->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + void CreateRemoveTransitionCommandIfNecessary(bool next); + + Track* track_; + Block* block_; + + GapBlock* existing_gap_; + GapBlock* existing_merged_gap_; + bool existing_gap_precedes_; + GapBlock* our_gap_; + + NodeSetPositionAsChildCommand* position_command_; + + QObject memory_manager_; + + QVector transition_remove_commands_; + +}; + +class BlockEnableDisableCommand : public UndoCommand { +public: + BlockEnableDisableCommand(Block* block, bool enabled) : + block_(block), + old_enabled_(block_->is_enabled()), + new_enabled_(enabled) + { + } + + virtual Project* GetRelevantProject() const override + { + return block_->project(); + } + + virtual void redo() override + { + block_->set_enabled(new_enabled_); + } + + virtual void undo() override + { + block_->set_enabled(old_enabled_); + } + +private: + Block* block_; + + bool old_enabled_; + + bool new_enabled_; + +}; + +class TrackListInsertGaps : public UndoCommand { +public: + TrackListInsertGaps(TrackList* track_list, const rational& point, const rational& length) : + track_list_(track_list), + point_(point), + length_(length), + split_command_(nullptr) + { + } + + virtual ~TrackListInsertGaps() override + { + delete split_command_; + } + + virtual Project* GetRelevantProject() const override + { + return track_list_->parent()->project(); + } + + virtual void prepare() override; + + virtual void redo() override; + + virtual void undo() override; + +private: + TrackList* track_list_; + + rational point_; + + rational length_; + + QVector working_tracks_; + + bool all_tracks_unlocked_; + + QVector gaps_to_extend_; + + struct AddGap { + GapBlock* gap; + Block* before; + Track* track; + }; + + QVector gaps_added_; + + BlockSplitPreservingLinksCommand* split_command_; + + QObject memory_manager_; + +}; + +} + +#endif // TIMELINEUNDOGENERAL_H diff --git a/app/widget/timelinewidget/undo/timelineundopointer.cpp b/app/widget/timelinewidget/undo/timelineundopointer.cpp new file mode 100644 index 000000000..0bac3b4af --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundopointer.cpp @@ -0,0 +1,439 @@ +/*** + + 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 "timelineundopointer.h" + +#include "node/block/gap/gap.h" +#include "node/block/transition/transition.h" +#include "node/graph.h" +#include "timelineundocommon.h" + +namespace olive { + +// +// BlockTrimCommand +// +void BlockTrimCommand::redo() +{ + if (doing_nothing_) { + return; + } + + // Begin an operation since we'll be doing a lot + track_->BeginOperation(); + + // Determine how much time to invalidate + TimeRange invalidate_range; + + if (mode_ == Timeline::kTrimIn) { + invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff_); + block_->set_length_and_media_in(new_length_); + } else { + invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff_); + block_->set_length_and_media_out(new_length_); + } + + if (needs_adjacent_) { + if (we_created_adjacent_) { + // Add adjacent and insert it + adjacent_->setParent(track_->parent()); + + if (mode_ == Timeline::kTrimIn) { + track_->InsertBlockBefore(adjacent_, block_); + } else { + track_->InsertBlockAfter(adjacent_, block_); + } + } else if (we_removed_adjacent_) { + track_->RippleRemoveBlock(adjacent_); + + // It no longer inputs/outputs anything, remove it + if (remove_block_from_graph_ && NodeCanBeRemoved(adjacent_)) { + if (!deleted_adjacent_command_) { + deleted_adjacent_command_ = CreateAndRunRemoveCommand(adjacent_); + } else { + deleted_adjacent_command_->redo(); + } + } + } else { + rational adjacent_length = adjacent_->length() + trim_diff_; + + if (mode_ == Timeline::kTrimIn) { + adjacent_->set_length_and_media_out(adjacent_length); + } else { + adjacent_->set_length_and_media_in(adjacent_length); + } + } + } + + track_->EndOperation(); + + if (dynamic_cast(block_)) { + // Whole transition needs to be invalidated + invalidate_range = block_->range(); + } + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); +} + +void BlockTrimCommand::undo() +{ + if (doing_nothing_) { + return; + } + + track_->BeginOperation(); + + // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer + if (needs_adjacent_) { + if (we_created_adjacent_) { + // Adjacent is ours, just delete it + track_->RippleRemoveBlock(adjacent_); + adjacent_->setParent(&memory_manager_); + } else { + if (we_removed_adjacent_) { + if (deleted_adjacent_command_) { + // We deleted adjacent, restore it now + deleted_adjacent_command_->undo(); + } + + if (mode_ == Timeline::kTrimIn) { + track_->InsertBlockBefore(adjacent_, block_); + } else { + track_->InsertBlockAfter(adjacent_, block_); + } + } else { + rational adjacent_length = adjacent_->length() - trim_diff_; + + if (mode_ == Timeline::kTrimIn) { + adjacent_->set_length_and_media_out(adjacent_length); + } else { + adjacent_->set_length_and_media_in(adjacent_length); + } + } + } + } + + 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_); + } + + if (dynamic_cast(block_)) { + // Whole transition needs to be invalidated + invalidate_range = block_->range(); + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); +} + +void BlockTrimCommand::prepare() +{ + // Store old length + old_length_ = block_->length(); + + // Determine if the length isn't changing, in which case we set a flag to do nothing + if ((doing_nothing_ = (old_length_ == new_length_))) { + return; + } + + // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer + trim_diff_ = old_length_ - new_length_; + + // Retrieve our adjacent block (or nullptr if none) + if (mode_ == Timeline::kTrimIn) { + adjacent_ = block_->previous(); + } else { + adjacent_ = block_->next(); + } + + // Ignore when trimming the out with no adjacent, because the user must have trimmed the end + // of the last block in the track, so we don't need to do anything elses + needs_adjacent_ = (mode_ == Timeline::kTrimIn || adjacent_); + + if (needs_adjacent_) { + // If we're trimming shorter, we need an adjacent, so check if we have a viable one. + we_created_adjacent_ = (trim_diff_ > 0 && (!adjacent_ || (!dynamic_cast(adjacent_) && !trim_is_a_roll_edit_))); + + if (we_created_adjacent_) { + // We shortened but don't have a viable adjacent to lengthen, so we create one + adjacent_ = new GapBlock(); + adjacent_->set_length_and_media_out(trim_diff_); + } else { + // Determine if we're removing the adjacent + rational adjacent_length = adjacent_->length() + trim_diff_; + we_removed_adjacent_ = adjacent_length.isNull(); + } + } +} + +// +// TrackSlideCommand +// +void TrackSlideCommand::redo() +{ + // Make sure all movement blocks' old positions are invalidated + TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); + + track_->BeginOperation(); + + // We will always have an in adjacent if there was a valid slide + if (we_created_in_adjacent_) { + // We created in adjacent, so all we have to do is insert it + in_adjacent_->setParent(track_->parent()); + track_->InsertBlockBefore(in_adjacent_, blocks_.first()); + } else if (-movement_ == in_adjacent_->length()) { + // Movement will remove in adjacent + track_->RippleRemoveBlock(in_adjacent_); + + if (NodeCanBeRemoved(in_adjacent_)) { + if (!in_adjacent_remove_command_) { + in_adjacent_remove_command_ = CreateRemoveCommand(in_adjacent_); + } + + in_adjacent_remove_command_->redo(); + } + } else { + // Simply resize adjacent + in_adjacent_->set_length_and_media_out(in_adjacent_->length() + movement_); + } + + // We may not have an out adjacent if the slide was at the end of the track + if (out_adjacent_) { + if (we_created_out_adjacent_) { + // We created out adjacent, so we just have to insert it + out_adjacent_->setParent(track_->parent()); + track_->InsertBlockAfter(out_adjacent_, blocks_.last()); + } else if (movement_ == out_adjacent_->length()) { + // Movement will remove out adjacent + track_->RippleRemoveBlock(out_adjacent_); + + if (NodeCanBeRemoved(out_adjacent_)) { + if (!out_adjacent_remove_command_) { + out_adjacent_remove_command_ = CreateRemoveCommand(out_adjacent_); + } + + out_adjacent_remove_command_->redo(); + } + } else { + // Simply resize adjacent + out_adjacent_->set_length_and_media_in(out_adjacent_->length() - movement_); + } + } + + track_->EndOperation(); + + // Make sure all movement blocks' new positions are invalidated + invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), + qMax(invalidate_range.out(), blocks_.last()->out())); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); +} + + +void TrackSlideCommand::undo() +{ + // Make sure all movement blocks' old positions are invalidated + TimeRange invalidate_range(blocks_.first()->in(), blocks_.last()->out()); + + track_->BeginOperation(); + + if (we_created_in_adjacent_) { + // We created this, so we can remove it now + track_->RippleRemoveBlock(in_adjacent_); + in_adjacent_->setParent(&memory_manager_); + } else if (in_adjacent_remove_command_) { + // We removed this, so we can restore it now + in_adjacent_remove_command_->undo(); + } else { + // Simply resize adjacent + in_adjacent_->set_length_and_media_out(in_adjacent_->length() - movement_); + } + + if (out_adjacent_) { + if (we_created_out_adjacent_) { + // We created this, so we can remove it now + track_->RippleRemoveBlock(out_adjacent_); + out_adjacent_->setParent(&memory_manager_); + } else if (out_adjacent_remove_command_) { + out_adjacent_remove_command_->undo(); + } else { + out_adjacent_->set_length_and_media_in(out_adjacent_->length() + movement_); + } + } + + track_->EndOperation(); + + // Make sure all movement blocks' new positions are invalidated + invalidate_range.set_range(qMin(invalidate_range.in(), blocks_.first()->in()), + qMax(invalidate_range.out(), blocks_.last()->out())); + + track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput); +} + +void TrackSlideCommand::prepare() +{ + if (!in_adjacent_) { + in_adjacent_ = new GapBlock(); + in_adjacent_->set_length_and_media_out(movement_); + in_adjacent_->setParent(&memory_manager_); + we_created_in_adjacent_ = true; + } else { + we_created_in_adjacent_ = false; + } + + if (!out_adjacent_ && blocks_.last()->next()) { + out_adjacent_ = new GapBlock(); + out_adjacent_->set_length_and_media_out(-movement_); + out_adjacent_->setParent(&memory_manager_); + we_created_out_adjacent_ = true; + } else { + we_created_out_adjacent_ = false; + } +} + +// +// TrackPlaceBlockCommand +// +TrackPlaceBlockCommand::~TrackPlaceBlockCommand() +{ + delete ripple_remove_command_; + qDeleteAll(add_track_commands_); + qDeleteAll(position_commands_); +} + +void TrackPlaceBlockCommand::redo() +{ + TimeRangeList ranges_to_invalidate; + + // Determine if we need to add tracks + if (track_index_ >= timeline_->GetTracks().size()) { + if (add_track_commands_.isEmpty()) { + // First redo, create tracks now + add_track_commands_.resize(track_index_ - timeline_->GetTracks().size() + 1); + + for (int i=0; iredo(); + } + } + + Track* track = timeline_->GetTrackAt(track_index_); + + track->BeginOperation(); + + bool 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 + if (!gap_) { + gap_ = new GapBlock(); + gap_->set_length_and_media_out(in_ - track->track_length()); + } + gap_->setParent(track->parent()); + track->AppendBlock(gap_); + ranges_to_invalidate.insert(gap_->range()); + } + + track->AppendBlock(insert_); + + if (position_commands_.isEmpty()) { + // Create position commands for insert and gap if necessary + if (gap_) { + position_commands_.append(new NodeSetPositionAsChildCommand(gap_, track, track, gap_->index(), track->Blocks().size(), true)); + } + position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); + } + } else { + // Place the Block at this point + if (!ripple_remove_command_) { + ripple_remove_command_ = new TrackRippleRemoveAreaCommand(track, TimeRange(in_, in_ + insert_->length())); + + } + + ripple_remove_command_->redo(); + track->InsertBlockAfter(insert_, ripple_remove_command_->GetInsertionIndex()); + + if (position_commands_.isEmpty()) { + position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); + } + } + + track->EndOperation(); + + ranges_to_invalidate.insert(insert_->range()); + + foreach (const TimeRange &r, ranges_to_invalidate) { + track->Node::InvalidateCache(r, Track::kBlockInput); + } + + for (int i=0; iredo(); + } +} + +void TrackPlaceBlockCommand::undo() +{ + for (int i=position_commands_.size()-1; i>=0; i--) { + position_commands_.at(i)->undo(); + } + + Track* t = timeline_->GetTrackAt(track_index_); + + TimeRange insert_range(insert_->in(), insert_->out()); + + // Firstly, remove our insert + t->BeginOperation(); + t->RippleRemoveBlock(insert_); + + if (ripple_remove_command_) { + // If we ripple removed, just undo that + ripple_remove_command_->undo(); + } else if (gap_) { + t->RippleRemoveBlock(gap_); + gap_->setParent(&memory_manager_); + } + t->EndOperation(); + + if (ripple_remove_command_) { + t->Node::InvalidateCache(insert_range, Track::kBlockInput); + } + + // Remove tracks if we added them + for (int i=add_track_commands_.size()-1; i>=0; i--) { + add_track_commands_.at(i)->undo(); + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundopointer.h b/app/widget/timelinewidget/undo/timelineundopointer.h new file mode 100644 index 000000000..d3abf3a6f --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundopointer.h @@ -0,0 +1,204 @@ +/*** + + 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 TIMELINEUNDOPOINTER_H +#define TIMELINEUNDOPOINTER_H + +#include "node/block/gap/gap.h" +#include "node/output/track/track.h" +#include "node/output/track/tracklist.h" +#include "node/project/sequence/sequence.h" +#include "timelineundogeneral.h" +#include "timelineundoripple.h" + +namespace olive { + +/** + * @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) : + track_(track), + block_(block), + new_length_(new_length), + mode_(mode), + deleted_adjacent_command_(nullptr), + trim_is_a_roll_edit_(false) + { + } + + 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 prepare() override; + virtual void redo() override; + virtual void undo() override; + +private: + 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 TrackSlideCommand : public UndoCommand { +public: + TrackSlideCommand(Track* track, const QList& moving_blocks, Block* in_adjacent, Block* out_adjacent, const rational& movement) : + track_(track), + blocks_(moving_blocks), + movement_(movement), + in_adjacent_(in_adjacent), + in_adjacent_remove_command_(nullptr), + out_adjacent_(out_adjacent), + out_adjacent_remove_command_(nullptr) + { + Q_ASSERT(!movement_.isNull()); + } + + virtual ~TrackSlideCommand() override + { + delete in_adjacent_remove_command_; + delete out_adjacent_remove_command_; + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void prepare() override; + + virtual void redo() override; + + virtual void undo() override; + +private: + Track* track_; + QList blocks_; + rational movement_; + + bool we_created_in_adjacent_; + Block* in_adjacent_; + UndoCommand* in_adjacent_remove_command_; + bool we_created_out_adjacent_; + Block* out_adjacent_; + UndoCommand* out_adjacent_remove_command_; + + QObject memory_manager_; + +}; + +/** + * @brief Destructively places `block` at the in point `start` + * + * The Block is guaranteed to be placed at the starting point specified. If there are Blocks in this area, they are + * either trimmed or removed to make space for this Block. Additionally, if the Block is placed beyond the end of + * the Sequence, a GapBlock is inserted to compensate. + */ +class TrackPlaceBlockCommand : public UndoCommand { +public: + TrackPlaceBlockCommand(TrackList *timeline, int track, Block* block, rational in) : + timeline_(timeline), + track_index_(track), + in_(in), + gap_(nullptr), + insert_(block), + ripple_remove_command_(nullptr) + { + } + + virtual ~TrackPlaceBlockCommand() override; + + virtual Project* GetRelevantProject() const override + { + return timeline_->parent()->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + TrackList* timeline_; + int track_index_; + rational in_; + GapBlock* gap_; + Block* insert_; + QVector add_track_commands_; + QObject memory_manager_; + TrackRippleRemoveAreaCommand* ripple_remove_command_; + QVector position_commands_; + +}; + +} + +#endif // TIMELINEUNDOPOINTER_H diff --git a/app/widget/timelinewidget/undo/timelineundoripple.cpp b/app/widget/timelinewidget/undo/timelineundoripple.cpp new file mode 100644 index 000000000..78b377cae --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoripple.cpp @@ -0,0 +1,500 @@ +/*** + + 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 "timelineundoripple.h" + +#include "timelineundocommon.h" + +namespace olive { + +// +// TrackRippleRemoveAreaCommand +// +TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range) : + track_(track), + range_(range), + 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) { + // 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() +{ + 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); +} + +void TrackRippleRemoveAreaCommand::undo() +{ + // 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); +} + +// +// TrackListRippleRemoveAreaCommand +// +void TrackListRippleRemoveAreaCommand::redo() +{ + // 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, range_); + 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(range_.out(), range_.in()); + } else if (list_->type() == Track::kAudio) { + list_->parent()->ShiftAudioCache(range_.out(), range_.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(); + } + } +} + +void TrackListRippleRemoveAreaCommand::undo() +{ + 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(range_.in(), range_.out()); + } else if (list_->type() == Track::kAudio) { + list_->parent()->ShiftAudioCache(range_.in(), range_.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(); + track->Node::InvalidateCache(range_, Track::kBlockInput); + } + } +} + +// +// TimelineRippleRemoveAreaCommand +// +TimelineRippleRemoveAreaCommand::TimelineRippleRemoveAreaCommand(Sequence* timeline, rational in, rational out) : + timeline_(timeline) +{ + for (int i=0; itrack_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) +{ + all_tracks_unlocked_ = (info_.size() == track_list_->GetTrackCount()); +} + +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; + + // Begin operation so we can invalidate better later + track->BeginOperation(); + + 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); + } + + if (all_tracks_unlocked_) { + // We rippled all the tracks, so we can shift the whole cache + if (track_list_->type() == Track::kVideo) { + track_list_->parent()->ShiftVideoCache(pre_latest_out, post_latest_out); + } else if (track_list_->type() == Track::kAudio) { + track_list_->parent()->ShiftAudioCache(pre_latest_out, post_latest_out); + } + } + + for (auto it=working_data_.cbegin(); it!=working_data_.cend(); it++) { + Track* track = it.key(); + + track->EndOperation(); + + if (!all_tracks_unlocked_) { + // If we're not shifting, the whole track must get invalidated + track->Node::InvalidateCache(TimeRange(it.value().earliest_point_of_change, RATIONAL_MAX), Track::kBlockInput); + } + } +} + +// +// TimelineRippleDeleteGapsAtRegionsCommand +// +void TimelineRippleDeleteGapsAtRegionsCommand::redo() +{ + if (commands_.isEmpty()) { + foreach (const TimeRange& range, regions_) { + rational max_ripple_length = range.length(); + + QVector blocks_around_range; + + foreach (Track* 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 (dynamic_cast(block_at_time)) { + 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 + commands_.append(new TrackRippleRemoveBlockCommand(resize->track(), resize)); + } else { + // Resize block + commands_.append(new BlockResizeCommand(resize, resize->length() - max_ripple_length)); + } + } + } + } + } + + foreach (UndoCommand* c, commands_) { + c->redo(); + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundoripple.h b/app/widget/timelinewidget/undo/timelineundoripple.h new file mode 100644 index 000000000..c98cdc03b --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoripple.h @@ -0,0 +1,237 @@ +/*** + + 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 TIMELINEUNDORIPPLE_H +#define TIMELINEUNDORIPPLE_H + +#include "node/block/gap/gap.h" +#include "node/output/track/track.h" +#include "node/output/track/tracklist.h" +#include "node/project/sequence/sequence.h" +#include "timelineundogeneral.h" +#include "timelineundosplit.h" +#include "timelineundotrack.h" + +namespace olive { + +/** + * @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); + + virtual ~TrackRippleRemoveAreaCommand() override; + + 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 prepare() override; + + virtual void redo() override; + + virtual void undo() override; + +private: + 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), + range_(in, out) + { + } + + virtual ~TrackListRippleRemoveAreaCommand() override + { + qDeleteAll(commands_); + } + + virtual Project* GetRelevantProject() const override + { + return list_->parent()->project(); + } + + virtual void redo() override; + + virtual void undo() override; + +private: + TrackList* list_; + + QList working_tracks_; + + TimeRange range_; + + bool all_tracks_unlocked_; + + QVector commands_; + +}; + +class TimelineRippleRemoveAreaCommand : public MultiUndoCommand { +public: + TimelineRippleRemoveAreaCommand(Sequence* timeline, rational in, rational out); + + virtual Project* GetRelevantProject() const override + { + return timeline_->project(); + } + +private: + Sequence* timeline_; + +}; + +class TrackListRippleToolCommand : public UndoCommand { +public: + struct RippleInfo { + Block* block; + bool append_gap; + }; + + TrackListRippleToolCommand(TrackList* track_list, + const QHash& info, + const rational& ripple_movement, + const Timeline::MovementMode& movement_mode); + + virtual Project* GetRelevantProject() const override + { + return track_list_->parent()->project(); + } + + virtual void redo() override + { + ripple(true); + } + + virtual void undo() override + { + ripple(false); + } + +private: + void ripple(bool redo); + + TrackList* track_list_; + + QHash info_; + rational ripple_movement_; + Timeline::MovementMode movement_mode_; + + struct WorkingData { + GapBlock* created_gap = nullptr; + Block* removed_gap_after; + rational old_length; + rational earliest_point_of_change; + }; + + QHash working_data_; + + QObject memory_manager_; + + bool all_tracks_unlocked_; + +}; + +class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { +public: + TimelineRippleDeleteGapsAtRegionsCommand(Sequence* vo, const TimeRangeList& regions) : + timeline_(vo), + regions_(regions) + { + } + + virtual ~TimelineRippleDeleteGapsAtRegionsCommand() override + { + qDeleteAll(commands_); + } + + virtual Project* GetRelevantProject() const override + { + return timeline_->project(); + } + + virtual void redo() override; + + virtual void undo() override + { + for (int i=commands_.size()-1;i>=0;i--) { + commands_.at(i)->undo(); + } + } + +private: + Sequence* timeline_; + TimeRangeList regions_; + + QVector commands_; + +}; + +} + +#endif // TIMELINEUNDORIPPLE_H diff --git a/app/widget/timelinewidget/undo/timelineundosplit.cpp b/app/widget/timelinewidget/undo/timelineundosplit.cpp new file mode 100644 index 000000000..93018596a --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundosplit.cpp @@ -0,0 +1,181 @@ +/*** + + 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 "timelineundosplit.h" + +#include "node/block/transition/transition.h" +#include "widget/nodeview/nodeviewundo.h" + +namespace olive { + +// +// BlockSplitCommand +// +void BlockSplitCommand::redo() +{ + 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, nullptr, 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(); +} + +void BlockSplitCommand::undo() +{ + 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(); +} + +// +// BlockSplitPreservingLinksCommand +// +void BlockSplitPreservingLinksCommand::redo() +{ + 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(); + } + } +} + +// +// TrackSplitAtTimeCommand +// +void TrackSplitAtTimeCommand::prepare() +{ + // Find Block that contains this time + Block* b = track_->BlockContainingTime(point_); + + if (b) { + command_ = new BlockSplitCommand(b, point_); + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundosplit.h b/app/widget/timelinewidget/undo/timelineundosplit.h new file mode 100644 index 000000000..6e0c1e6d6 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundosplit.h @@ -0,0 +1,159 @@ +/*** + + 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 TIMELINEUNDOSPLIT_H +#define TIMELINEUNDOSPLIT_H + +#include "node/output/track/track.h" + +namespace olive { + +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; + + virtual void undo() override; + +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; + + 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) : + track_(track), + point_(point), + command_(nullptr) + { + } + + virtual ~TrackSplitAtTimeCommand() override + { + delete command_; + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void prepare() override; + + virtual void redo() override + { + if (command_) { + command_->redo(); + } + } + + virtual void undo() override + { + if (command_) { + command_->undo(); + } + } + +private: + Track* track_; + + rational point_; + + UndoCommand* command_; + +}; + +} + +#endif // TIMELINEUNDOSPLIT_H diff --git a/app/widget/timelinewidget/undo/timelineundotrack.cpp b/app/widget/timelinewidget/undo/timelineundotrack.cpp new file mode 100644 index 000000000..7e14c10b1 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundotrack.cpp @@ -0,0 +1,25 @@ +/*** + + 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 "timelineundotrack.h" + +namespace olive { + +} diff --git a/app/widget/timelinewidget/undo/timelineundotrack.h b/app/widget/timelinewidget/undo/timelineundotrack.h new file mode 100644 index 000000000..8e6706f7c --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundotrack.h @@ -0,0 +1,163 @@ +/*** + + 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 TIMELINEUNDOTRACK_H +#define TIMELINEUNDOTRACK_H + +#include "node/output/track/track.h" + +namespace olive { + +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_; +}; + +/** + * @brief Replaces Block `old` with Block `replace` + * + * Both blocks must have equal lengths. + */ +class TrackReplaceBlockCommand : public UndoCommand +{ +public: + TrackReplaceBlockCommand(Track* track, Block* old, Block* replace) : + track_(track), + old_(old), + replace_(replace) + { + } + + virtual Project* GetRelevantProject() const override + { + return track_->project(); + } + + virtual void redo() override + { + track_->ReplaceBlock(old_, replace_); + } + + virtual void undo() override + { + track_->ReplaceBlock(replace_, old_); + } + +private: + Track* track_; + Block* old_; + Block* replace_; + +}; + +} + +#endif // TIMELINEUNDOTRACK_H diff --git a/app/widget/timelinewidget/undo/timelineundoworkarea.cpp b/app/widget/timelinewidget/undo/timelineundoworkarea.cpp new file mode 100644 index 000000000..fcdd9b8b1 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoworkarea.cpp @@ -0,0 +1,25 @@ +/*** + + 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 "timelineundoworkarea.h" + +namespace olive { + +} diff --git a/app/widget/timelinewidget/undo/timelineundoworkarea.h b/app/widget/timelinewidget/undo/timelineundoworkarea.h new file mode 100644 index 000000000..b44437a47 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoworkarea.h @@ -0,0 +1,103 @@ +/*** + + 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 TIMELINEUNDOWORKAREA_H +#define TIMELINEUNDOWORKAREA_H + +#include "node/project/project.h" +#include "timeline/timelinepoints.h" + +namespace olive { + +class WorkareaSetEnabledCommand : public UndoCommand { +public: + WorkareaSetEnabledCommand(Project *project, TimelinePoints* points, bool enabled) : + project_(project), + points_(points), + old_enabled_(points_->workarea()->enabled()), + new_enabled_(enabled) + { + } + + virtual Project* GetRelevantProject() const override + { + return project_; + } + + virtual void redo() override + { + points_->workarea()->set_enabled(new_enabled_); + } + + virtual void undo() override + { + points_->workarea()->set_enabled(old_enabled_); + } + +private: + Project* project_; + + TimelinePoints* points_; + + bool old_enabled_; + + bool new_enabled_; + +}; + +class WorkareaSetRangeCommand : public UndoCommand { +public: + WorkareaSetRangeCommand(Project *project, TimelinePoints* points, const TimeRange& range) : + project_(project), + points_(points), + old_range_(points_->workarea()->range()), + new_range_(range) + { + } + + virtual Project* GetRelevantProject() const override + { + return project_; + } + + virtual void redo() override + { + points_->workarea()->set_range(new_range_); + } + + virtual void undo() override + { + points_->workarea()->set_range(old_range_); + } + +private: + Project* project_; + + TimelinePoints* points_; + + TimeRange old_range_; + + TimeRange new_range_; + +}; + +} + +#endif // TIMELINEUNDOWORKAREA_H diff --git a/tests/timeline/timeline-tests.cpp b/tests/timeline/timeline-tests.cpp index 529168e30..17df4ee03 100644 --- a/tests/timeline/timeline-tests.cpp +++ b/tests/timeline/timeline-tests.cpp @@ -26,7 +26,8 @@ #include "node/project/project.h" #include "node/project/sequence/sequence.h" #include "undo/undocommand.h" -#include "widget/timelinewidget/timelineundo.h" +#include "widget/timelinewidget/undo/timelineundogeneral.h" +#include "widget/timelinewidget/undo/timelineundopointer.h" #include "testutil.h" namespace olive {