From 9fa22938b9cc91aecf9fd5e7b33e977f3c5cef99 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 23 Sep 2019 23:00:15 +1000 Subject: [PATCH] work towards making timeline actions all undoable --- app/common/timecodefunctions.cpp | 41 +- app/common/timecodefunctions.h | 6 +- app/decoder/ffmpeg/ffmpegdecoder.cpp | 3 +- app/node/block/block.cpp | 13 + app/node/block/block.h | 5 +- app/node/graph.cpp | 4 +- app/node/node.cpp | 9 + app/node/node.h | 5 + app/node/output/timeline/timeline.cpp | 98 +---- app/node/output/timeline/timeline.h | 33 +- app/node/output/track/track.cpp | 160 -------- app/node/output/track/track.h | 57 +-- app/node/param.cpp | 11 + app/node/param.h | 5 + app/project/projectviewmodel.cpp | 6 +- app/undo/undostack.cpp | 11 +- app/undo/undostack.h | 12 +- app/widget/nodeview/nodeviewitem.cpp | 6 +- app/widget/timelineview/CMakeLists.txt | 1 + app/widget/timelineview/timelineview.cpp | 31 +- app/widget/timelineview/timelineview.h | 22 +- app/widget/timelineview/tool/CMakeLists.txt | 1 + app/widget/timelineview/tool/import.cpp | 21 +- app/widget/timelineview/tool/pointer.cpp | 28 +- app/widget/timelineview/tool/razor.cpp | 22 +- app/widget/timelineview/tool/ripple.cpp | 23 +- app/widget/timelineview/tool/rolling.cpp | 95 +++++ app/widget/timelineview/undo/CMakeLists.txt | 22 ++ app/widget/timelineview/undo/undo.cpp | 416 ++++++++++++++++++++ app/widget/timelineview/undo/undo.h | 202 ++++++++++ app/widget/viewer/viewer.cpp | 3 +- 31 files changed, 994 insertions(+), 378 deletions(-) create mode 100644 app/widget/timelineview/tool/rolling.cpp create mode 100644 app/widget/timelineview/undo/CMakeLists.txt create mode 100644 app/widget/timelineview/undo/undo.cpp create mode 100644 app/widget/timelineview/undo/undo.h diff --git a/app/common/timecodefunctions.cpp b/app/common/timecodefunctions.cpp index 94b4ee3b0..1a4037e6f 100644 --- a/app/common/timecodefunctions.cpp +++ b/app/common/timecodefunctions.cpp @@ -28,7 +28,8 @@ QString padded(int arg, int padding) { QString olive::timestamp_to_timecode(const int64_t ×tamp, const rational& timebase, - const TimecodeDisplay& display) + const TimecodeDisplay& display, + bool show_plus_if_positive) { double timestamp_dbl = (rational(timestamp) * timebase).toDouble(); @@ -36,6 +37,16 @@ QString olive::timestamp_to_timecode(const int64_t ×tamp, case kTimecodeFrames: case kTimecodeSeconds: { + QString prefix; + + if (timestamp_dbl < 0) { + prefix = "-"; + } else if (show_plus_if_positive) { + prefix = "+"; + } + + timestamp_dbl = qAbs(timestamp_dbl); + int total_seconds = qFloor(timestamp_dbl); int hours = total_seconds / 3600; @@ -45,19 +56,21 @@ QString olive::timestamp_to_timecode(const int64_t ×tamp, if (display == kTimecodeSeconds) { int fraction = qRound((timestamp_dbl - total_seconds) * 1000); - return QString("%1:%2:%3.%4").arg(padded(hours, 2), - padded(mins, 2), - padded(secs, 2), - padded(fraction, 3)); + return QString("%1%2:%3:%4.%5").arg(prefix, + padded(hours, 2), + padded(mins, 2), + padded(secs, 2), + padded(fraction, 3)); } else { rational frame_rate = timebase.flipped(); int frames = qRound((timestamp_dbl - total_seconds) * frame_rate.toDouble()); - return QString("%1:%2:%3;%4").arg(padded(hours, 2), - padded(mins, 2), - padded(secs, 2), - padded(frames, 2)); + return QString("%1%2:%3:%4;%5").arg(prefix, + padded(hours, 2), + padded(mins, 2), + padded(secs, 2), + padded(frames, 2)); } } case kFrames: @@ -68,3 +81,13 @@ QString olive::timestamp_to_timecode(const int64_t ×tamp, return QString(); } + +rational olive::timestamp_to_time(const int64_t ×tamp, const rational &timebase) +{ + return rational(timestamp) * timebase; +} + +int64_t olive::time_to_timestamp(const rational &time, const rational &timebase) +{ + return qRound64(time.toDouble() * timebase.flipped().toDouble()); +} diff --git a/app/common/timecodefunctions.h b/app/common/timecodefunctions.h index d731631f2..4afa189c1 100644 --- a/app/common/timecodefunctions.h +++ b/app/common/timecodefunctions.h @@ -37,7 +37,11 @@ enum TimecodeDisplay { /** * @brief Convert a timestamp (according to a rational timebase) to a user-friendly string representation */ -QString timestamp_to_timecode(const int64_t ×tamp, const rational& timebase, const TimecodeDisplay& display); +QString timestamp_to_timecode(const int64_t ×tamp, const rational& timebase, const TimecodeDisplay& display, bool show_plus_if_positive = false); + +int64_t time_to_timestamp(const rational& time, const rational& timebase); + +rational timestamp_to_time(const int64_t& timestamp, const rational& timebase); } diff --git a/app/decoder/ffmpeg/ffmpegdecoder.cpp b/app/decoder/ffmpeg/ffmpegdecoder.cpp index 9fd29b377..c7b437df5 100644 --- a/app/decoder/ffmpeg/ffmpegdecoder.cpp +++ b/app/decoder/ffmpeg/ffmpegdecoder.cpp @@ -31,6 +31,7 @@ extern "C" { #include #include "common/filefunctions.h" +#include "common/timecodefunctions.h" #include "render/pixelservice.h" FFmpegDecoder::FFmpegDecoder() : @@ -326,7 +327,7 @@ int64_t FFmpegDecoder::GetTimestampFromTime(const rational &time) } // Convert timecode to AVStream timebase - int64_t target_ts = qRound64(time.toDouble() * rational(avstream_->time_base).flipped().toDouble()); + int64_t target_ts = olive::time_to_timestamp(time, avstream_->time_base); // Find closest actual timebase in the file target_ts = GetClosestTimestampInIndex(target_ts); diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index e4baa0251..018e51407 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -75,6 +75,15 @@ void Block::set_length(const rational &length) Refresh(); } +void Block::set_length_and_media_in(const rational &length) +{ + // Calculate media_in adjustment + set_media_in(media_in_ + (length_ - length)); + + // Set the length + set_length(length); +} + Block *Block::previous() { return ValueToPtr(previous_input_->get_value(0)); @@ -171,12 +180,16 @@ const rational &Block::media_in() void Block::set_media_in(const rational &media_in) { + Lock(); + if (media_in_ != media_in) { media_in_ = media_in; // Signal that this clips contents have changed SendInvalidateCache(in(), out()); } + + Unlock(); } const QString &Block::block_name() diff --git a/app/node/block/block.h b/app/node/block/block.h index e5034c10a..61e86cef4 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -50,8 +50,9 @@ public: const rational& in(); const rational& out(); - virtual const rational &length(); - virtual void set_length(const rational &length); + const rational &length(); + void set_length(const rational &length); + void set_length_and_media_in(const rational &length); Block* previous(); Block* next(); diff --git a/app/node/graph.cpp b/app/node/graph.cpp index 50905efd2..1e71aca58 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -59,11 +59,13 @@ void NodeGraph::TakeNode(Node *node, QObject* new_parent) return; } - node->setParent(new_parent); + node->DisconnectAll(); disconnect(node, SIGNAL(EdgeAdded(NodeEdgePtr)), this, SIGNAL(EdgeAdded(NodeEdgePtr))); disconnect(node, SIGNAL(EdgeRemoved(NodeEdgePtr)), this, SIGNAL(EdgeRemoved(NodeEdgePtr))); + node->setParent(new_parent); + emit NodeRemoved(node); } diff --git a/app/node/node.cpp b/app/node/node.cpp index e62e732fe..28c6b4d96 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -347,6 +347,15 @@ bool Node::HasConnectedOutputs() return HasParamOfType(NodeParam::kOutput, true); } +void Node::DisconnectAll() +{ + QList param = parameters(); + + for (int i=0;iDisconnectAll(); + } +} + void Node::Hash(QCryptographicHash *hash, NodeOutput* from, const rational &time) { // Add this Node's ID diff --git a/app/node/node.h b/app/node/node.h index 39c31a37b..5c3faa83f 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -173,6 +173,11 @@ public: */ bool HasConnectedOutputs(); + /** + * @brief Severs all input and output connections + */ + void DisconnectAll(); + /** * @brief Add's unique information about this Node at the given time to a QCryptographicHash */ diff --git a/app/node/output/timeline/timeline.cpp b/app/node/output/timeline/timeline.cpp index 73de262f5..556a8a30b 100644 --- a/app/node/output/timeline/timeline.cpp +++ b/app/node/output/timeline/timeline.cpp @@ -81,6 +81,11 @@ const QVector &TimelineOutput::Tracks() return track_cache_; } +TrackOutput *TimelineOutput::TrackAt(int index) +{ + return track_cache_.at(index); +} + QVariant TimelineOutput::Value(NodeOutput *output, const rational &time) { if (output == length_output_) { @@ -191,6 +196,19 @@ void TimelineOutput::AddTrack() } } +void TimelineOutput::RemoveTrack() +{ + if (track_cache_.isEmpty()) { + return; + } + + TrackOutput* track = track_cache_.last(); + + static_cast(parent())->TakeNode(track); + + delete track; +} + TrackOutput *TimelineOutput::TrackFromBlock(Block *block) { Block* n = block; @@ -263,83 +281,3 @@ void TimelineOutput::TrackEdgeRemoved(NodeEdgePtr edge) DetachTrack(added_track); } } - -void TimelineOutput::PlaceBlock(Block *block, rational start, int track) -{ - Q_ASSERT(track >= 0); - - while (track >= track_cache_.size()) { - AddTrack(); - } - - track_cache_.at(track)->PlaceBlock(block, start); -} - -void TimelineOutput::ReplaceBlock(Block *old, Block *replace, int track) -{ - track_cache_.at(track)->ReplaceBlock(old, replace); -} - -void TimelineOutput::SplitAtTime(rational time, int track) -{ - if (track >= track_cache_.size()) { - return; - } - - track_cache_.at(track)->SplitAtTime(time); -} - -void TimelineOutput::ResizeBlock(Block *block, rational new_length) -{ - block->set_length(new_length); -} - -void TimelineOutput::RippleBlocks(QList blocks, rational ripple_length, olive::timeline::MovementMode mode) -{ - if (blocks.isEmpty() - || ripple_length == 0 - || (mode != olive::timeline::kTrimIn && mode != olive::timeline::kTrimOut)) { - return; - } - - if (mode == olive::timeline::kTrimIn) { - // Flip the ripple length if we're trimming the in point - ripple_length = -ripple_length; - } - - QVector rippled_tracks; - - rational ripple_point = RATIONAL_MAX; - - // Ripple each Block as requested - foreach (Block* b, blocks) { - if (mode == olive::timeline::kTrimIn) { - ripple_point = qMin(ripple_point, b->in()); - - // Extend media in point - b->set_media_in(b->media_in() - ripple_length); - } else { - ripple_point = qMin(ripple_point, b->out()); - } - - b->set_length(b->length() + ripple_length); - - rippled_tracks.append(TrackFromBlock(b)); - } - - // For each track that did not have a rippled clip, insert a Gap to keep all tracks synchronized - // FIXME: Assumes rippling out point further out - foreach (TrackOutput* track, track_cache_) { - - if (!rippled_tracks.contains(track)) { - Block* block_after_time = track->NearestBlockAfter(ripple_point); - - if (block_after_time != nullptr) { - // Insert Gap block before this Block - GapBlock* gap = new GapBlock(); - gap->set_length(ripple_length); - track->InsertBlockBefore(gap, block_after_time); - } - } - } -} diff --git a/app/node/output/timeline/timeline.h b/app/node/output/timeline/timeline.h index dcea69238..a9bae133c 100644 --- a/app/node/output/timeline/timeline.h +++ b/app/node/output/timeline/timeline.h @@ -48,6 +48,12 @@ public: const QVector& Tracks(); + TrackOutput* TrackAt(int index); + + void AddTrack(); + + void RemoveTrack(); + public slots: /** * @brief Slot for when the track connection is added @@ -79,31 +85,6 @@ public slots: */ void TrackEdgeRemoved(NodeEdgePtr edge); - /** - * @brief Forwards a PlaceBlock signal to the requested track - * - * If the track index doesn't exist, tracks are automatically created until a track at that index does exist - * (provided the track index is positive). A negative track index fails immediately. - */ - void PlaceBlock(Block* block, rational start, int track); - - /** - * @brief Forwards a ReplaceBlock signal to the appropriate track - */ - void ReplaceBlock(Block* old, Block* replace, int track); - - /** - * @brief Forwards a SplitAtTime signal to the appropriate track - */ - void SplitAtTime(rational time, int track); - - /** - * @brief Resizes a Block - */ - void ResizeBlock(Block* block, rational new_length); - - void RippleBlocks(QList blocks, rational ripple_length, olive::timeline::MovementMode mode); - signals: void TimebaseChanged(const rational &timebase); @@ -129,8 +110,6 @@ private: void DetachTrack(TrackOutput* track); - void AddTrack(); - static TrackOutput* TrackFromBlock(Block* block); NodeInput* track_input_; diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 94b056bfe..017e39d0d 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -71,11 +71,6 @@ QString TrackOutput::Description() "a Sequence."); } -void TrackOutput::set_length(const rational &) -{ - // Prevent length changing on this Block -} - void TrackOutput::Refresh() { QVector detect_attached_blocks; @@ -337,31 +332,6 @@ void TrackOutput::UnblockInvalidateCache() block_invalidate_cache_stack_--; } -void TrackOutput::PlaceBlock(Block *block, rational start) -{ - if (block_cache_.contains(block) && block->in() == start) { - return; - } - - AddBlockToGraph(block); - - // Check if the placement location is past the end of the timeline - if (start >= in()) { - if (start > in()) { - // If so, insert a gap here - GapBlock* gap = new GapBlock(); - gap->set_length(start - in()); - AppendBlock(gap); - } - - AppendBlock(block); - return; - } - - // Place the Block at this point - RippleRemoveArea(start, start + block->length(), block); -} - void TrackOutput::RemoveBlock(Block *block) { GapBlock* gap = new GapBlock(); @@ -409,136 +379,6 @@ void TrackOutput::RippleRemoveBlock(Block *block) // FIXME: Should there be removing the Blocks from the graph? } -Block* TrackOutput::SplitBlock(Block *block, rational time) -{ - if (time <= block->in() || time >= block->out()) { - return nullptr; - } - - BlockInvalidateCache(); - - rational original_length = block->length(); - - block->set_length(time - block->in()); - - Block* copy = block->copy(); - copy->set_length(original_length - block->length()); - copy->set_media_in(block->media_in() + block->length()); - - InsertBlockAfter(copy, block); - - Node::CopyInputs(block, copy); - - UnblockInvalidateCache(); - - return copy; -} - -void TrackOutput::SplitAtTime(rational time) -{ - // Find Block that contains this time - for (int i=0;iout() == time) { - // This time is between blocks, no split needs to occur - return; - } else if (b->in() < time && b->out() > time) { - // We found the Block, split it - SplitBlock(b, time); - return; - } - } -} - -void TrackOutput::RippleRemoveArea(rational in, rational out, Block *insert) -{ - // Block that needs to be split to remove this area - Block* splice = nullptr; - - // Block whose out point exceeds `in` and needs to be trimmed - Block* trim_out_to_in = nullptr; - - // Block whose in point exceeds `out` and needs to be trimmed - Block* trim_in_to_out = nullptr; - - // Blocks that are entirely within the area and need removing - QList remove; - - // Iterate through blocks determining which need trimming/removing/splitting - foreach (Block* block, block_cache_) { - if (block->in() < in && block->out() > out) { - // The area entirely within this Block - splice = block; - - // We don't need to do anything else here - break; - } else if (block->in() >= in && block->out() <= out) { - // This Block's is entirely within the area - remove.append(block); - } else if (block->in() < in && block->out() >= in) { - // This Block's out point exceeds `in` - trim_out_to_in = block; - } else if (block->in() <= out && block->out() > out) { - // This Block's in point exceeds `out` - trim_in_to_out = block; - } - } - - BlockInvalidateCache(); - - // If we picked up a block to splice - if (splice != nullptr) { - - // Split the block here - Block* copy = SplitBlock(splice, in); - - // Perform all further actions as if we were just trimming these clips - trim_out_to_in = splice; - trim_in_to_out = copy; - - } - - // If we picked up a block to trim the in point of - if (trim_in_to_out != nullptr && trim_in_to_out->in() < out) { - rational new_length = trim_in_to_out->out() - out; - - // Push media_in forward to compensate - rational length_diff = trim_in_to_out->length() - new_length; - trim_in_to_out->set_media_in(trim_in_to_out->media_in() - length_diff); - - trim_in_to_out->set_length(new_length); - } - - // Remove all blocks that are flagged for removal - foreach (Block* remove_block, remove) { - RippleRemoveBlock(remove_block); - } - - // If we picked up a block to trim the out point of - if (trim_out_to_in != nullptr && trim_out_to_in->out() > in) { - trim_out_to_in->set_length(in - trim_out_to_in->in()); - } - - // If we were given a block to insert, insert it here - if (insert != nullptr) { - if (trim_out_to_in == nullptr) { - // This is the start of the Sequence - PrependBlock(insert); - } else if (trim_in_to_out == nullptr) { - // This is the end of the Sequence - AppendBlock(insert); - } else { - // This is somewhere in the middle of the Sequence - InsertBlockBetweenBlocks(insert, trim_out_to_in, trim_in_to_out); - } - } - - UnblockInvalidateCache(); - - InvalidateCache(in, out); -} - void TrackOutput::ReplaceBlock(Block *old, Block *replace) { Q_ASSERT(old->length() == replace->length()); diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index be923392a..5129a02cc 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -41,8 +41,6 @@ public: virtual QString Category() override; virtual QString Description() override; - virtual void set_length(const rational &length) override; - virtual void Refresh() override; const int& Index(); @@ -104,15 +102,6 @@ public: */ void AppendBlock(Block* block); - /** - * @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. - */ - void PlaceBlock(Block* block, rational start); - /** * @brief Removes a Block and places a Gap in its place */ @@ -123,31 +112,6 @@ public: */ void RippleRemoveBlock(Block* block); - /** - * @brief Splits `block` into two Blocks at the Sequence point `time` - * - * @return - * - * The second block created as a result of this split - */ - Block *SplitBlock(Block* block, rational time); - - /** - * @brief Attempt to split at a certain time - * - * Finds the Block that surrounds this time and splits it. If there is no Block there, this is a no-op. - */ - void SplitAtTime(rational time); - - /** - * @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`. - */ - void RippleRemoveArea(rational in, rational out, Block* insert = nullptr); - /** * @brief Replaces Block `old` with Block `replace` * @@ -155,6 +119,17 @@ public: */ void ReplaceBlock(Block* old, Block* replace); + void BlockInvalidateCache(); + + void UnblockInvalidateCache(); + + /** + * @brief Adds a Block to the parent graph so it can be connected to other Nodes + * + * Also runs through Node's dependencies (the Nodes whose outputs are connected to this Node's inputs) + */ + void AddBlockToGraph(Block* block); + signals: /** * @brief Signal emitted when a Block is added to this Track @@ -180,21 +155,11 @@ private: */ void RemoveBlockInternal(); - /** - * @brief Adds a Block to the parent graph so it can be connected to other Nodes - * - * Also runs through Node's dependencies (the Nodes whose outputs are connected to this Node's inputs) - */ - void AddBlockToGraph(Block* block); - /** * @brief Sets current_block_ to the correct attached Block based on `time` */ void ValidateCurrentBlock(const rational& time); - void BlockInvalidateCache(); - void UnblockInvalidateCache(); - QVector block_cache_; Block* current_block_; diff --git a/app/node/param.cpp b/app/node/param.cpp index 0d9c5f37e..8a793db14 100644 --- a/app/node/param.cpp +++ b/app/node/param.cpp @@ -78,6 +78,13 @@ const QVector &NodeParam::edges() return edges_; } +void NodeParam::DisconnectAll() +{ + while (!edges_.isEmpty()) { + DisconnectEdge(edges_.first()); + } +} + bool NodeParam::AreDataTypesCompatible(NodeParam *a, NodeParam *b) { // Make sure one is an input and one is an output @@ -159,6 +166,10 @@ NodeEdgePtr NodeParam::ConnectEdge(NodeOutput *output, NodeInput *input) NodeEdgePtr edge = std::make_shared(output, input); + // The nodes should never be the same, and since we lock both nodes here, this can lead to a entire program freeze + // that's difficult to diagnose. This makes that issue very clear. + Q_ASSERT(output->parent() != input->parent()); + output->parent()->Lock(); input->parent()->Lock(); diff --git a/app/node/param.h b/app/node/param.h index b4d65957b..d11d2ba31 100644 --- a/app/node/param.h +++ b/app/node/param.h @@ -157,6 +157,11 @@ public: */ const QVector& edges(); + /** + * @brief Disconnect any edges connecting this parameter to other parameters + */ + void DisconnectAll(); + /** * @brief Determine whether two DataTypes are compatible and therefore whether two NodeParams can be connected * diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp index 52e2459c6..c89841512 100644 --- a/app/project/projectviewmodel.cpp +++ b/app/project/projectviewmodel.cpp @@ -340,11 +340,7 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action } } - if (move_command->childCount() > 0) { - olive::undo_stack.push(move_command); - } else { - delete move_command; - } + olive::undo_stack.pushIfHasChildren(move_command); return true; diff --git a/app/undo/undostack.cpp b/app/undo/undostack.cpp index 74fea6a08..5e290e17e 100644 --- a/app/undo/undostack.cpp +++ b/app/undo/undostack.cpp @@ -1,3 +1,12 @@ #include "undostack.h" -QUndoStack olive::undo_stack; +OliveUndoStack olive::undo_stack; + +void OliveUndoStack::pushIfHasChildren(QUndoCommand *command) +{ + if (command->childCount() > 0) { + push(command); + } else { + delete command; + } +} diff --git a/app/undo/undostack.h b/app/undo/undostack.h index 0aabcf787..754e9fb12 100644 --- a/app/undo/undostack.h +++ b/app/undo/undostack.h @@ -3,11 +3,21 @@ #include +class OliveUndoStack : public QUndoStack { +public: + /** + * @brief A wrapper for push() that either pushes if the command has children or deletes if not + * + * This function takes ownership of `command`, and may delete it so it should never be accessed after this call. + */ + void pushIfHasChildren(QUndoCommand* command); +}; + namespace olive { /** * @brief A static undo stack for undoable commands throughout Olive */ -extern QUndoStack undo_stack; +extern OliveUndoStack undo_stack; } #endif // UNDOSTACK_H diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 74386e2a2..0027d9c7a 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -458,11 +458,7 @@ void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) dragging_edge_ = nullptr; - if (node_edge_change_command_->childCount() > 0) { - olive::undo_stack.push(node_edge_change_command_); - } else { - delete node_edge_change_command_; - } + olive::undo_stack.pushIfHasChildren(node_edge_change_command_); node_edge_change_command_ = nullptr; return; } diff --git a/app/widget/timelineview/CMakeLists.txt b/app/widget/timelineview/CMakeLists.txt index d3327fc2a..0f1056e3e 100644 --- a/app/widget/timelineview/CMakeLists.txt +++ b/app/widget/timelineview/CMakeLists.txt @@ -15,6 +15,7 @@ # along with this program. If not, see . add_subdirectory(tool) +add_subdirectory(undo) set(OLIVE_SOURCES ${OLIVE_SOURCES} diff --git a/app/widget/timelineview/timelineview.cpp b/app/widget/timelineview/timelineview.cpp index eaf47a47c..cde32a592 100644 --- a/app/widget/timelineview/timelineview.cpp +++ b/app/widget/timelineview/timelineview.cpp @@ -37,6 +37,7 @@ TimelineView::TimelineView(QWidget *parent) : pointer_tool_(this), import_tool_(this), ripple_tool_(this), + rolling_tool_(this), razor_tool_(this), hand_tool_(this), zoom_tool_(this), @@ -65,20 +66,20 @@ void TimelineView::AddBlock(Block *block, int track) case Block::kClip: case Block::kGap: { - TimelineViewBlockItem* clip_item = new TimelineViewBlockItem(); + TimelineViewBlockItem* item = new TimelineViewBlockItem(); // Set up clip with view parameters (clip item will automatically size its rect accordingly) - clip_item->SetBlock(block); - clip_item->SetY(GetTrackY(track)); - clip_item->SetHeight(GetTrackHeight(track)); - clip_item->SetScale(scale_); - clip_item->SetTrack(track); + item->SetBlock(block); + item->SetY(GetTrackY(track)); + item->SetHeight(GetTrackHeight(track)); + item->SetScale(scale_); + item->SetTrack(track); // Add to list of clip items that can be iterated through - clip_items_.insert(block, clip_item); + block_items_.insert(block, item); // Add item to graphics scene - scene_.addItem(clip_item); + scene_.addItem(item); connect(block, SIGNAL(Refreshed()), this, SLOT(BlockChanged())); break; @@ -93,9 +94,9 @@ void TimelineView::AddBlock(Block *block, int track) void TimelineView::RemoveBlock(Block *block) { - delete clip_items_[block]; + delete block_items_[block]; - clip_items_.remove(block); + block_items_.remove(block); } void TimelineView::AddTrack(TrackOutput *track) @@ -116,7 +117,7 @@ void TimelineView::SetScale(const double &scale) { scale_ = scale; - QMapIterator iterator(clip_items_); + QMapIterator iterator(block_items_); while (iterator.hasNext()) { iterator.next(); @@ -143,7 +144,7 @@ void TimelineView::SetTimebase(const rational &timebase) void TimelineView::Clear() { - QMapIterator iterator(clip_items_); + QMapIterator iterator(block_items_); while (iterator.hasNext()) { iterator.next(); @@ -153,7 +154,7 @@ void TimelineView::Clear() } } - clip_items_.clear(); + block_items_.clear(); } void TimelineView::ConnectTimelineNode(TimelineOutput *node) @@ -265,7 +266,7 @@ TimelineView::Tool *TimelineView::GetActiveTool() case olive::tool::kRipple: return &ripple_tool_; case olive::tool::kRolling: - return nullptr; // FIXME: Implement + return &rolling_tool_; case olive::tool::kRazor: return &razor_tool_; case olive::tool::kSlip: @@ -353,7 +354,7 @@ void TimelineView::ClearGhosts() void TimelineView::BlockChanged() { - TimelineViewRect* rect = clip_items_[static_cast(sender())]; + TimelineViewRect* rect = block_items_[static_cast(sender())]; if (rect != nullptr) { rect->UpdateRect(); diff --git a/app/widget/timelineview/timelineview.h b/app/widget/timelineview/timelineview.h index c117e8ff0..592e4f05b 100644 --- a/app/widget/timelineview/timelineview.h +++ b/app/widget/timelineview/timelineview.h @@ -32,6 +32,8 @@ #include "timelineviewblockitem.h" #include "timelineviewghostitem.h" #include "timelineviewplayheaditem.h" +#include "widget/timelineview/undo/undo.h" +#include "undo/undostack.h" /** * @brief A widget for viewing and interacting Sequences @@ -230,6 +232,9 @@ private: virtual void MousePress(QMouseEvent *event); virtual void MouseMove(QMouseEvent *event); virtual void MouseRelease(QMouseEvent *event); + + private: + QVector split_tracks_; }; class RippleTool : public PointerTool @@ -245,6 +250,20 @@ private: bool allow_gap_trimming) override; }; + class RollingTool : public PointerTool + { + public: + RollingTool(TimelineView* parent); + + protected: + virtual void MouseReleaseInternal(QMouseEvent *event) override; + virtual rational FrameValidateInternal(rational time_movement, const QVector& ghosts) override; + + virtual void InitiateGhosts(TimelineViewBlockItem* clicked_item, + olive::timeline::MovementMode trim_mode, + bool allow_gap_trimming) override; + }; + class HandTool : public Tool { public: @@ -277,6 +296,7 @@ private: PointerTool pointer_tool_; ImportTool import_tool_; RippleTool ripple_tool_; + RollingTool rolling_tool_; RazorTool razor_tool_; HandTool hand_tool_; ZoomTool zoom_tool_; @@ -302,7 +322,7 @@ private: int64_t playhead_; - QMap clip_items_; + QMap block_items_; QVector ghost_items_; diff --git a/app/widget/timelineview/tool/CMakeLists.txt b/app/widget/timelineview/tool/CMakeLists.txt index 6a635dba6..854ac72ba 100644 --- a/app/widget/timelineview/tool/CMakeLists.txt +++ b/app/widget/timelineview/tool/CMakeLists.txt @@ -21,6 +21,7 @@ set(OLIVE_SOURCES widget/timelineview/tool/pointer.cpp widget/timelineview/tool/razor.cpp widget/timelineview/tool/ripple.cpp + widget/timelineview/tool/rolling.cpp widget/timelineview/tool/tool.cpp widget/timelineview/tool/zoom.cpp PARENT_SCOPE diff --git a/app/widget/timelineview/tool/import.cpp b/app/widget/timelineview/tool/import.cpp index 881708290..5e6580657 100644 --- a/app/widget/timelineview/tool/import.cpp +++ b/app/widget/timelineview/tool/import.cpp @@ -21,6 +21,7 @@ #include "widget/timelineview/timelineview.h" #include +#include #include "config/config.h" #include "common/qtversionabstraction.h" @@ -131,6 +132,8 @@ void TimelineView::ImportTool::DragMove(QDragMoveEvent *event) SnapPoint(snap_points_, &time_movement); } + rational earliest_ghost = RATIONAL_MAX; + // Move ghosts to the mouse cursor foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { ghost->SetInAdjustment(time_movement); @@ -140,8 +143,19 @@ void TimelineView::ImportTool::DragMove(QDragMoveEvent *event) ghost->SetY(ghost_y); ghost->SetHeight(ghost_height); + + earliest_ghost = qMin(earliest_ghost, ghost->GetAdjustedIn()); } + // Generate tooltip (showing earliest in point of imported clip) + int64_t earliest_timestamp = olive::time_to_timestamp(earliest_ghost, parent()->timebase_); + QString tooltip_text = olive::timestamp_to_timecode(earliest_timestamp, + parent()->timebase_, + kTimecodeDisplay); + QToolTip::showText(QCursor::pos(), + tooltip_text, + parent()); + event->accept(); } else { event->ignore(); @@ -166,6 +180,8 @@ void TimelineView::ImportTool::DragDrop(QDropEvent *event) // of scope will delete the nodes. If there is, they'll become parents of the NodeGraph instead QObject node_memory_manager; + QUndoCommand* command = new QUndoCommand(); + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { ClipBlock* clip = new ClipBlock(); MediaInput* media = new MediaInput(); @@ -187,13 +203,16 @@ void TimelineView::ImportTool::DragDrop(QDropEvent *event) NodeParam::ConnectEdge(media->texture_output(), opacity->texture_input()); NodeParam::ConnectEdge(transform->matrix_output(), media->matrix_input()); + if (event->keyboardModifiers() & Qt::ControlModifier) { //emit parent()->RequestInsertBlockAtTime(clip, ghost->GetAdjustedIn()); } else { - parent()->timeline_node_->PlaceBlock(clip, ghost->GetAdjustedIn(), ghost->Track()); + new TrackPlaceBlockCommand(parent()->timeline_node_, ghost->Track(), clip, ghost->GetAdjustedIn(), command); } } + olive::undo_stack.pushIfHasChildren(command); + parent()->ClearGhosts(); event->accept(); diff --git a/app/widget/timelineview/tool/pointer.cpp b/app/widget/timelineview/tool/pointer.cpp index 922340519..6c912b762 100644 --- a/app/widget/timelineview/tool/pointer.cpp +++ b/app/widget/timelineview/tool/pointer.cpp @@ -21,9 +21,12 @@ #include "widget/timelineview/timelineview.h" #include +#include #include "common/clamp.h" #include "common/range.h" +#include "common/timecodefunctions.h" +#include "config/config.h" #include "core.h" #include "node/block/gap/gap.h" @@ -112,6 +115,8 @@ void TimelineView::PointerTool::MouseReleaseInternal(QMouseEvent *event) // get cleaned up if they aren't re-parented by the attached NodeGraph QObject block_memory_manager; + QUndoCommand* command = new QUndoCommand(); + // Since all the ghosts will be leaving their old position in some way, we replace all of them with gaps here so the // entire timeline isn't disrupted in the process foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { @@ -122,7 +127,7 @@ void TimelineView::PointerTool::MouseReleaseInternal(QMouseEvent *event) gap->setParent(&block_memory_manager); gap->set_length(b->length()); - parent()->timeline_node_->ReplaceBlock(b, gap, ghost->Track()); + new TrackReplaceBlockCommand(parent()->timeline_node_->TrackAt(ghost->Track()), b, gap, command); } // Now we place the clips back in the timeline where the user moved them. It's legal for them to overwrite parts or @@ -136,14 +141,16 @@ void TimelineView::PointerTool::MouseReleaseInternal(QMouseEvent *event) // If we were trimming the in point, we'll need to adjust the media in too if (ghost->mode() == olive::timeline::kTrimIn) { - b->set_media_in(b->media_in() + ghost->InAdjustment()); + new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command); + } else { + new BlockResizeCommand(b, ghost->AdjustedLength(), command); } - - b->set_length(ghost->AdjustedLength()); } - parent()->timeline_node_->PlaceBlock(b, ghost->GetAdjustedIn(), ghost->GetAdjustedTrack()); + new TrackPlaceBlockCommand(parent()->timeline_node_, ghost->GetAdjustedTrack(), b, ghost->GetAdjustedIn(), command); } + + olive::undo_stack.pushIfHasChildren(command); } rational TimelineView::PointerTool::FrameValidateInternal(rational time_movement, const QVector& ghosts) @@ -248,6 +255,17 @@ void TimelineView::PointerTool::ProcessDrag(const QPoint &mouse_pos) } } } + + // Show tooltip + // Generate tooltip (showing earliest in point of imported clip) + int64_t earliest_timestamp = olive::time_to_timestamp(time_movement, parent()->timebase_); + QString tooltip_text = olive::timestamp_to_timecode(earliest_timestamp, + parent()->timebase_, + kTimecodeDisplay, + true); + QToolTip::showText(QCursor::pos(), + tooltip_text, + parent()); } void TimelineView::PointerTool::InitiateGhosts(TimelineViewBlockItem* clicked_item, diff --git a/app/widget/timelineview/tool/razor.cpp b/app/widget/timelineview/tool/razor.cpp index 5d7a04736..1eab31cf6 100644 --- a/app/widget/timelineview/tool/razor.cpp +++ b/app/widget/timelineview/tool/razor.cpp @@ -27,6 +27,8 @@ TimelineView::RazorTool::RazorTool(TimelineView* parent) : void TimelineView::RazorTool::MousePress(QMouseEvent *event) { + split_tracks_.clear(); + MouseMove(event); } @@ -39,18 +41,30 @@ void TimelineView::RazorTool::MouseMove(QMouseEvent *event) QPointF current_scene_pos = GetScenePos(event->pos()); - // Always split at the same time - rational split_time = parent()->SceneToTime(drag_start_.x()); - // Split at the current cursor track int split_track = parent()->SceneToTrack(current_scene_pos.y()); - parent()->timeline_node_->SplitAtTime(split_time, split_track); + if (!split_tracks_.contains(split_track)) { + split_tracks_.append(split_track); + } } void TimelineView::RazorTool::MouseRelease(QMouseEvent *event) { Q_UNUSED(event) + // Always split at the same time + rational split_time = parent()->SceneToTime(drag_start_.x()); + + QUndoCommand* command = new QUndoCommand(); + + foreach (int track, split_tracks_) { + new TrackSplitAtTimeCommand(parent()->timeline_node_->TrackAt(track), split_time, command); + } + + split_tracks_.clear(); + + olive::undo_stack.pushIfHasChildren(command); + dragging_ = false; } diff --git a/app/widget/timelineview/tool/ripple.cpp b/app/widget/timelineview/tool/ripple.cpp index 9ad23cd97..23f6fec7e 100644 --- a/app/widget/timelineview/tool/ripple.cpp +++ b/app/widget/timelineview/tool/ripple.cpp @@ -36,15 +36,10 @@ void TimelineView::RippleTool::MouseReleaseInternal(QMouseEvent *event) return; } - // Retrieve cursor position difference - QPointF scene_pos = GetScenePos(event->pos()); - QPointF movement = scene_pos - drag_start_; - // For ripple operations, all ghosts will be moving the same way olive::timeline::MovementMode movement_mode = parent()->ghost_items_.first()->mode(); - // The amount to ripple by - rational ripple_length = parent()->SceneToTime(movement.x()); + QUndoCommand* command = new QUndoCommand(); // Find earliest point to ripple around foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { @@ -60,24 +55,28 @@ void TimelineView::RippleTool::MouseReleaseInternal(QMouseEvent *event) Block* block_to_append_gap_to = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kReferenceBlock)); - parent()->timeline_node_->Tracks().at(ghost->Track())->InsertBlockAfter(gap, - block_to_append_gap_to); + new TrackInsertBlockBetweenBlocksCommand(parent()->timeline_node_->Tracks().at(ghost->Track()), + gap, + block_to_append_gap_to, + block_to_append_gap_to->next()); } } else { // This was a Block that already existed if (ghost->AdjustedLength() > 0) { - b->set_length(ghost->AdjustedLength()); - if (movement_mode == olive::timeline::kTrimIn) { // We'll need to shift the media in point too - b->set_media_in(b->media_in() + ghost->InAdjustment()); + new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command); + } else { + new BlockResizeCommand(b, ghost->AdjustedLength(), command); } } else { // Assumed the Block was a Gap and it was reduced to zero length, remove it here - parent()->timeline_node_->Tracks().at(ghost->Track())->RippleRemoveBlock(b); + new TrackRippleRemoveBlockCommand(parent()->timeline_node_->Tracks().at(ghost->Track()), b, command); } } } + + olive::undo_stack.pushIfHasChildren(command); } rational TimelineView::RippleTool::FrameValidateInternal(rational time_movement, const QVector &ghosts) diff --git a/app/widget/timelineview/tool/rolling.cpp b/app/widget/timelineview/tool/rolling.cpp new file mode 100644 index 000000000..6cb2a6eab --- /dev/null +++ b/app/widget/timelineview/tool/rolling.cpp @@ -0,0 +1,95 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 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 "widget/timelineview/timelineview.h" + +#include "node/block/gap/gap.h" + +TimelineView::RollingTool::RollingTool(TimelineView* parent) : + PointerTool(parent) +{ + SetMovementAllowed(false); +} + +void TimelineView::RollingTool::MouseReleaseInternal(QMouseEvent *event) +{ + Q_UNUSED(event) + + if (parent()->ghost_items_.isEmpty()) { + return; + } + + QUndoCommand* command = new QUndoCommand(); + + // Find earliest point to ripple around + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + if (ghost->mode() == olive::timeline::kTrimIn) { + if (b->previous() == nullptr) { + // We'll need to insert a gap here, so we'll do a Place command instead + GapBlock* gap = new GapBlock(); + gap->set_length(ghost->Length()); + + new TrackReplaceBlockCommand(parent()->timeline_node_->TrackAt(ghost->Track()), b, gap, command); + } + + new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command); + + if (b->previous() == nullptr) { + new TrackPlaceBlockCommand(parent()->timeline_node_, ghost->Track(), b, ghost->GetAdjustedIn(), command); + } + } else if (ghost->mode() == olive::timeline::kTrimOut) { + new BlockResizeCommand(b, ghost->AdjustedLength(), command); + } + } + + olive::undo_stack.pushIfHasChildren(command); +} + +rational TimelineView::RollingTool::FrameValidateInternal(rational time_movement, const QVector &ghosts) +{ + // Only validate trimming, and we don't care about "overwriting" since the rolling tool is designed to trim at collisions + time_movement = ValidateInTrimming(time_movement, ghosts, false); + time_movement = ValidateOutTrimming(time_movement, ghosts, false); + + return time_movement; +} + +void TimelineView::RollingTool::InitiateGhosts(TimelineViewBlockItem *clicked_item, + olive::timeline::MovementMode trim_mode, + bool allow_gap_trimming) +{ + Q_UNUSED(allow_gap_trimming) + + PointerTool::InitiateGhosts(clicked_item, trim_mode, true); + + // For each ghost, we make an equivalent Ghost on the next/previous block + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + Block* ghost_block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + if (ghost->mode() == olive::timeline::kTrimIn && ghost_block->previous() != nullptr) { + // Add an extra Ghost for the previous block + AddGhostFromBlock(ghost_block->previous(), ghost->Track(), olive::timeline::kTrimOut); + } else if (ghost->mode() == olive::timeline::kTrimOut && ghost_block->next() != nullptr && ghost_block->next()->type() != Block::kEnd) { + AddGhostFromBlock(ghost_block->next(), ghost->Track(), olive::timeline::kTrimIn); + } + } +} diff --git a/app/widget/timelineview/undo/CMakeLists.txt b/app/widget/timelineview/undo/CMakeLists.txt new file mode 100644 index 000000000..6232b7a44 --- /dev/null +++ b/app/widget/timelineview/undo/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 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/timelineview/undo/undo.h + widget/timelineview/undo/undo.cpp + PARENT_SCOPE +) diff --git a/app/widget/timelineview/undo/undo.cpp b/app/widget/timelineview/undo/undo.cpp new file mode 100644 index 000000000..107ea39cd --- /dev/null +++ b/app/widget/timelineview/undo/undo.cpp @@ -0,0 +1,416 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 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 "undo.h" + +#include "node/graph.h" + +Block* CreateSplitBlock(Block* block, rational point, QObject* parent = nullptr) +{ + Block* copy = block->copy(); + copy->set_length_and_media_in(block->length() - (point - block->in())); + copy->setParent(parent); + + return copy; +} + +Node* TakeNodeFromParentGraph(Node* n, QObject* new_parent = nullptr) { + static_cast(n->parent())->TakeNode(n, new_parent); + + return n; +} + +BlockResizeCommand::BlockResizeCommand(Block *block, rational new_length, QUndoCommand* parent) : + QUndoCommand(parent), + block_(block), + old_length_(block->length()), + new_length_(new_length) +{ +} + +void BlockResizeCommand::redo() +{ + block_->set_length(new_length_); +} + +void BlockResizeCommand::undo() +{ + block_->set_length(old_length_); +} + +BlockResizeWithMediaInCommand::BlockResizeWithMediaInCommand(Block *block, rational new_length, QUndoCommand *parent) : + QUndoCommand(parent), + block_(block), + old_length_(block->length()), + new_length_(new_length) +{ +} + +void BlockResizeWithMediaInCommand::redo() +{ + block_->set_length_and_media_in(new_length_); +} + +void BlockResizeWithMediaInCommand::undo() +{ + block_->set_length_and_media_in(old_length_); +} + +BlockSetMediaInCommand::BlockSetMediaInCommand(Block *block, rational new_media_in, QUndoCommand* parent) : + QUndoCommand(parent), + block_(block), + old_media_in_(block->media_in()), + new_media_in_(new_media_in) +{ +} + +void BlockSetMediaInCommand::redo() +{ + block_->set_media_in(new_media_in_); +} + +void BlockSetMediaInCommand::undo() +{ + block_->set_media_in(old_media_in_); +} + +TrackRippleRemoveBlockCommand::TrackRippleRemoveBlockCommand(TrackOutput *track, Block *block, QUndoCommand *parent) : + QUndoCommand(parent), + track_(track), + block_(block), + before_(block->previous()), + after_(block->next()) +{ +} + +void TrackRippleRemoveBlockCommand::redo() +{ + track_->RippleRemoveBlock(block_); +} + +void TrackRippleRemoveBlockCommand::undo() +{ + track_->InsertBlockBetweenBlocks(block_, before_, after_); +} + +TrackInsertBlockBetweenBlocksCommand::TrackInsertBlockBetweenBlocksCommand(TrackOutput *track, + Block *block, + Block *before, + Block *after, + QUndoCommand *parent) : + QUndoCommand(parent), + track_(track), + block_(block), + before_(before), + after_(after) +{ +} + +void TrackInsertBlockBetweenBlocksCommand::redo() +{ + track_->InsertBlockBetweenBlocks(block_, before_, after_); +} + +void TrackInsertBlockBetweenBlocksCommand::undo() +{ + track_->RippleRemoveBlock(block_); +} + +TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand(TrackOutput *track, rational in, rational out, QUndoCommand *parent) : + QUndoCommand(parent), + track_(track), + in_(in), + out_(out), + splice_(nullptr), + trim_out_(nullptr), + trim_in_(nullptr) +{ +} + +void TrackRippleRemoveAreaCommand::redo() +{ + // Iterate through blocks determining which need trimming/removing/splitting + foreach (Block* block, track_->Blocks()) { + if (block->in() < in_ && block->out() > out_) { + // The area entirely within this Block + splice_ = block; + + // We don't need to do anything else here + break; + } else if (block->in() >= in_ && block->out() <= out_) { + // This Block's is entirely within the area + removed_blocks_.append(block); + } else if (block->in() < in_ && block->out() >= in_) { + // This Block's out point exceeds `in` + trim_out_ = block; + } else if (block->in() <= out_ && block->out() > out_) { + // This Block's in point exceeds `out` + trim_in_ = block; + } + } + + // If we picked up a block to splice + if (splice_ != nullptr) { + + // Split the block here + Block* copy = CreateSplitBlock(splice_, in_); + + // Perform all further actions as if we were just trimming these clips + trim_out_ = splice_; + trim_in_ = copy; + + } + + // If we picked up a block to trim the in point of + if (trim_in_ != nullptr && trim_in_->in() < out_) { + trim_in_old_length_ = trim_in_->length(); + trim_in_new_length_ = trim_in_->out() - out_; + } + + // If we picked up a block to trim the out point of + if (trim_out_ != nullptr && trim_out_->out() > in_) { + trim_out_old_length_ = trim_out_->length(); + trim_out_new_length_ = in_ - trim_out_->in(); + } + + track_->BlockInvalidateCache(); + + // If we're splicing, trim_in_ is a copy + if (splice_ != nullptr) { + track_->AddBlockToGraph(trim_in_); + Node::CopyInputs(splice_, trim_in_); + + track_->InsertBlockAfter(trim_in_, splice_); + } + + // If we picked up a block to trim the in point of + if (trim_in_old_length_ != trim_in_new_length_) { + trim_in_->set_length_and_media_in(trim_in_new_length_); + } + + // Remove all blocks that are flagged for removal + foreach (Block* remove_block, removed_blocks_) { + track_->RippleRemoveBlock(remove_block); + } + + // If we picked up a block to trim the out point of + if (trim_out_old_length_ != trim_out_new_length_) { + trim_out_->set_length(trim_out_new_length_); + } + + // If we were given a block to insert, insert it here + if (insert_ != nullptr) { + track_->AddBlockToGraph(insert_); + + if (trim_out_ == nullptr) { + // This is the start of the Sequence + track_->PrependBlock(insert_); + } else if (trim_in_ == nullptr) { + // This is the end of the Sequence + track_->AppendBlock(insert_); + } else { + // This is somewhere in the middle of the Sequence + track_->InsertBlockBetweenBlocks(insert_, trim_out_, trim_in_); + } + } + + track_->UnblockInvalidateCache(); + + track_->InvalidateCache(in_, out_); +} + +void TrackRippleRemoveAreaCommand::undo() +{ + track_->BlockInvalidateCache(); + + // If we were given a block to insert, insert it here + if (insert_ != nullptr) { + track_->RippleRemoveBlock(insert_); + } + + // If we picked up a block to trim the out point of + if (trim_out_old_length_ != trim_out_new_length_) { + trim_out_->set_length(trim_out_old_length_); + } + + // Remove all blocks that are flagged for removal + foreach (Block* remove_block, removed_blocks_) { + if (trim_in_ == nullptr) { + track_->AppendBlock(remove_block); + } else { + track_->InsertBlockBefore(remove_block, trim_in_); + } + } + + // If we picked up a block to trim the in point of + if (trim_in_old_length_ != trim_in_new_length_) { + trim_in_->set_length_and_media_in(trim_in_old_length_); + } + + // If we're splicing, trim_in_ is a copy + if (splice_ != nullptr) { + track_->RippleRemoveBlock(trim_in_); + + // Remove node + TakeNodeFromParentGraph(trim_in_, &memory_manager_); + } + + track_->UnblockInvalidateCache(); + + track_->InvalidateCache(in_, out_); +} + +TrackPlaceBlockCommand::TrackPlaceBlockCommand(TimelineOutput* timeline, int track, Block *block, rational in, QUndoCommand *parent) : + TrackRippleRemoveAreaCommand(nullptr, in, 0, parent), // Out gets set correctly in redo() + timeline_(timeline), + track_index_(track), + gap_(nullptr) +{ + insert_ = block; + insert_->setParent(&memory_manager_); +} + +void TrackPlaceBlockCommand::redo() +{ + added_track_count_ = 0; + + // Get track (or make it if necessary) + while (track_index_ >= timeline_->Tracks().size()) { + timeline_->AddTrack(); + + added_track_count_++; + } + + track_ = timeline_->TrackAt(track_index_); + + append_ = (in_ >= track_->in()); + + // Check if the placement location is past the end of the timeline + if (append_) { + if (in_ > track_->in()) { + // If so, insert a gap here + gap_ = new GapBlock(); + gap_->set_length(in_ - track_->in()); + track_->AppendBlock(gap_); + } + + track_->AppendBlock(insert_); + } else { + out_ = in_ + insert_->length(); + + // Place the Block at this point + TrackRippleRemoveAreaCommand::redo(); + } +} + +void TrackPlaceBlockCommand::undo() +{ + if (append_) { + track_->RippleRemoveBlock(insert_); + TakeNodeFromParentGraph(insert_, &memory_manager_); + + if (gap_ != nullptr) { + track_->RippleRemoveBlock(gap_); + delete TakeNodeFromParentGraph(gap_); + } + } else { + TrackRippleRemoveAreaCommand::undo(); + } + + for (;added_track_count_>0;added_track_count_--) { + timeline_->RemoveTrack(); + } +} + +BlockSplitCommand::BlockSplitCommand(TrackOutput* track, Block *block, rational point, QUndoCommand *parent) : + QUndoCommand(parent), + track_(track), + block_(block), + new_length_(point - block->in()), + old_length_(block->length()) +{ + Q_ASSERT(point > block_->in() && point < block_->out()); + + // Ensures that this block is deleted if this action is undone + new_block_ = CreateSplitBlock(block_, point, &memory_manager_); +} + +void BlockSplitCommand::redo() +{ + track_->BlockInvalidateCache(); + + block_->set_length(new_length_); + + track_->AddBlockToGraph(new_block_); + Node::CopyInputs(block_, new_block_); + + // Will re-parent new_block_ to the track's graph + track_->InsertBlockAfter(new_block_, block_); + + track_->UnblockInvalidateCache(); +} + +void BlockSplitCommand::undo() +{ + track_->BlockInvalidateCache(); + + block_->set_length(old_length_); + track_->RippleRemoveBlock(new_block_); + + TakeNodeFromParentGraph(new_block_, &memory_manager_); + + track_->UnblockInvalidateCache(); +} + +TrackSplitAtTimeCommand::TrackSplitAtTimeCommand(TrackOutput *track, rational point, QUndoCommand *parent) : + QUndoCommand(parent) +{ + // Find Block that contains this time + for (int i=0;iBlocks().size();i++) { + Block* b = track->Blocks().at(i); + + if (b->out() == point) { + // This time is between blocks, no split needs to occur + return; + } else if (b->in() < point && b->out() > point) { + // We found the Block, split it + new BlockSplitCommand(track, b, point, this); + return; + } + } +} + +TrackReplaceBlockCommand::TrackReplaceBlockCommand(TrackOutput* track, Block *old, Block *replace, QUndoCommand *parent) : + QUndoCommand(parent), + track_(track), + old_(old), + replace_(replace) +{ +} + +void TrackReplaceBlockCommand::redo() +{ + track_->ReplaceBlock(old_, replace_); +} + +void TrackReplaceBlockCommand::undo() +{ + track_->ReplaceBlock(replace_, old_); +} diff --git a/app/widget/timelineview/undo/undo.h b/app/widget/timelineview/undo/undo.h new file mode 100644 index 000000000..786ee4a6b --- /dev/null +++ b/app/widget/timelineview/undo/undo.h @@ -0,0 +1,202 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 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 + +#include "node/block/block.h" +#include "node/block/gap/gap.h" +#include "node/output/timeline/timeline.h" +#include "node/output/track/track.h" + +class BlockResizeCommand : public QUndoCommand { +public: + BlockResizeCommand(Block* block, rational new_length, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +private: + Block* block_; + rational old_length_; + rational new_length_; +}; + +class BlockResizeWithMediaInCommand : public QUndoCommand { +public: + BlockResizeWithMediaInCommand(Block* block, rational new_length, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +private: + Block* block_; + rational old_length_; + rational new_length_; +}; + +class BlockSetMediaInCommand : public QUndoCommand { +public: + BlockSetMediaInCommand(Block* block, rational new_media_in, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +private: + Block* block_; + rational old_media_in_; + rational new_media_in_; +}; + +class TrackRippleRemoveBlockCommand : public QUndoCommand { +public: + TrackRippleRemoveBlockCommand(TrackOutput* track, Block* block, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +private: + TrackOutput* track_; + + Block* block_; + + Block* before_; + Block* after_; +}; + +class TrackInsertBlockBetweenBlocksCommand : public QUndoCommand { +public: + TrackInsertBlockBetweenBlocksCommand(TrackOutput* track, Block* block, Block* before, Block* after, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +private: + TrackOutput* track_; + + Block* block_; + + Block* before_; + Block* after_; +}; + +/** + * @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 QUndoCommand { +public: + TrackRippleRemoveAreaCommand(TrackOutput* track, rational in, rational out, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +protected: + TrackOutput* track_; + rational in_; + rational out_; + + Block* splice_; + + Block* trim_out_; + QVector removed_blocks_; + Block* trim_in_; + + rational trim_in_old_length_; + rational trim_out_old_length_; + + rational trim_in_new_length_; + rational trim_out_new_length_; + + Block* insert_; + + 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 TrackRippleRemoveAreaCommand { +public: + TrackPlaceBlockCommand(TimelineOutput *timeline, int track, Block* block, rational in, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +private: + TimelineOutput* timeline_; + int track_index_; + bool append_; + GapBlock* gap_; + int added_track_count_; +}; + +class BlockSplitCommand : public QUndoCommand { +public: + BlockSplitCommand(TrackOutput* track, Block* block, rational point, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +private: + TrackOutput* track_; + Block* block_; + + rational new_length_; + rational old_length_; + + Block* new_block_; + + QObject memory_manager_; +}; + +class TrackSplitAtTimeCommand : public QUndoCommand { +public: + TrackSplitAtTimeCommand(TrackOutput* track, rational point, QUndoCommand* parent = nullptr); +}; + +/** + * @brief Replaces Block `old` with Block `replace` + * + * Both blocks must have equal lengths. + */ +class TrackReplaceBlockCommand : public QUndoCommand { +public: + TrackReplaceBlockCommand(TrackOutput* track, Block* old, Block* replace, QUndoCommand* parent = nullptr); + + virtual void redo() override; + virtual void undo() override; + +private: + TrackOutput* track_; + Block* old_; + Block* replace_; +}; + +#endif // TIMELINEUNDOABLE_H diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 52bb15da9..8c05ad8c2 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -26,6 +26,7 @@ #include #include +#include "common/timecodefunctions.h" #include "viewersizer.h" ViewerWidget::ViewerWidget(QWidget *parent) : @@ -94,7 +95,7 @@ const double &ViewerWidget::scale() rational ViewerWidget::GetTime() { - return rational(ruler_->GetTime()) * time_base_; + return olive::timestamp_to_time(ruler_->GetTime(), time_base_); } void ViewerWidget::SetScale(const double &scale_)