diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp index 2127aaaf6..0e005bcb7 100644 --- a/app/audio/audiovisualwaveform.cpp +++ b/app/audio/audiovisualwaveform.cpp @@ -197,11 +197,11 @@ void AudioVisualWaveform::Shift(const rational &from, const rational &to) int to_index = time_to_samples(to, rate_dbl); if (from_index == to_index) { - return; + continue; } if (from_index > data.size()) { - return; + continue; } if (from_index > to_index) { @@ -226,7 +226,7 @@ void AudioVisualWaveform::Shift(const rational &from, const rational &to) memcpy(temp.data(), &data.data()[from_index], temp.size()); memcpy(&data.data()[to_index], temp.data(), temp.size()); - memset(reinterpret_cast(&data[from_index]), 0, distance * sizeof(SamplePerChannel)); + memset(&data.data()[from_index], 0, distance * sizeof(SamplePerChannel)); } } diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index c1b54c5aa..a2926f15d 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -396,7 +396,7 @@ bool FFmpegEncoder::WriteSubtitle(const SubtitleBlock *sub_block) subtitle.num_rects = 1; subtitle.rects = &rect_array; - subtitle.pts = Timecode::time_to_timestamp(sub_block->in(), av_get_time_base_q(), true); + subtitle.pts = Timecode::time_to_timestamp(sub_block->in(), av_get_time_base_q(), Timecode::kFloor); subtitle.end_display_time = qRound64(sub_block->length().toDouble() * 1000); QVector out_buf(1024 * 1024); diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index 3524a50c2..2dd2eaf62 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -36,6 +36,8 @@ set(OLIVE_SOURCES common/flipmodifiers.cpp common/flipmodifiers.h common/functiontimer.h + common/jobtime.cpp + common/jobtime.h common/lerp.h common/memorypool.h common/ocioutils.cpp diff --git a/app/common/jobtime.cpp b/app/common/jobtime.cpp new file mode 100644 index 000000000..43077ebf1 --- /dev/null +++ b/app/common/jobtime.cpp @@ -0,0 +1,30 @@ +#include "jobtime.h" + +#include + +namespace olive { + +uint64_t job_time_index = 0; +QMutex job_time_mutex; + +JobTime::JobTime() +{ + Acquire(); +} + +void JobTime::Acquire() +{ + job_time_mutex.lock(); + + value_ = job_time_index; + job_time_index++; + + job_time_mutex.unlock(); +} + +} + +QDebug operator<<(QDebug debug, const olive::JobTime& r) +{ + return debug.space() << r.value(); +} diff --git a/app/common/jobtime.h b/app/common/jobtime.h new file mode 100644 index 000000000..27988b92e --- /dev/null +++ b/app/common/jobtime.h @@ -0,0 +1,62 @@ +#ifndef JOBTIME_H +#define JOBTIME_H + +#include +#include + +namespace olive { + +class JobTime +{ +public: + JobTime(); + + void Acquire(); + + uint64_t value() const + { + return value_; + } + + bool operator==(const JobTime &rhs) const + { + return value_ == rhs.value_; + } + + bool operator!=(const JobTime &rhs) const + { + return value_ != rhs.value_; + } + + bool operator<(const JobTime &rhs) const + { + return value_ < rhs.value_; + } + + bool operator>(const JobTime &rhs) const + { + return value_ > rhs.value_; + } + + bool operator<=(const JobTime &rhs) const + { + return value_ <= rhs.value_; + } + + bool operator>=(const JobTime &rhs) const + { + return value_ >= rhs.value_; + } + +private: + uint64_t value_; + +}; + +} + +QDebug operator<<(QDebug debug, const olive::JobTime& r); + +Q_DECLARE_METATYPE(olive::JobTime) + +#endif // JOBTIME_H diff --git a/app/common/timecodefunctions.cpp b/app/common/timecodefunctions.cpp index 0501d5bb4..a90b9e023 100644 --- a/app/common/timecodefunctions.cpp +++ b/app/common/timecodefunctions.cpp @@ -239,7 +239,7 @@ rational Timecode::timecode_to_time(const QString &timecode, const rational &tim return timestamp_to_time(timestamp, timebase); } -rational Timecode::snap_time_to_timebase(const rational &time, const rational &timebase, bool floor) +rational Timecode::snap_time_to_timebase(const rational &time, const rational &timebase, Rounding floor) { // Just convert to a timestamp in timebase units and back int64_t timestamp = time_to_timestamp(time, timebase, floor); @@ -275,19 +275,23 @@ QString Timecode::TimeToString(int64_t ms) .arg(ss, 2, 10, QChar('0')); } -int64_t Timecode::time_to_timestamp(const rational &time, const rational &timebase, bool floor) +int64_t Timecode::time_to_timestamp(const rational &time, const rational &timebase, Rounding floor) { return time_to_timestamp(time.toDouble(), timebase, floor); } -int64_t Timecode::time_to_timestamp(const double &time, const rational &timebase, bool floor) +int64_t Timecode::time_to_timestamp(const double &time, const rational &timebase, Rounding floor) { double d = time * timebase.flipped().toDouble(); - if (floor) { - return qFloor(d); - } else { + switch (floor) { + case kRound: + default: return qRound64(d); + case kFloor: + return qFloor(d); + case kCeil: + return qCeil(d); } } diff --git a/app/common/timecodefunctions.h b/app/common/timecodefunctions.h index dea645b1c..7252f44db 100644 --- a/app/common/timecodefunctions.h +++ b/app/common/timecodefunctions.h @@ -47,6 +47,12 @@ public: kMilliseconds }; + enum Rounding { + kCeil, + kFloor, + kRound + }; + /** * @brief Convert a timestamp (according to a rational timebase) to a user-friendly string representation */ @@ -55,10 +61,10 @@ public: static int64_t timecode_to_timestamp(const QString& timecode, const rational& timebase, const Display& display, bool *ok = nullptr); static rational timecode_to_time(const QString& timecode, const rational& timebase, const Display& display, bool *ok = nullptr); - static rational snap_time_to_timebase(const rational& time, const rational& timebase, bool floor = false); + static rational snap_time_to_timebase(const rational& time, const rational& timebase, Rounding floor = kRound); - static int64_t time_to_timestamp(const rational& time, const rational& timebase, bool floor = false); - static int64_t time_to_timestamp(const double& time, const rational& timebase, bool floor = false); + static int64_t time_to_timestamp(const rational& time, const rational& timebase, Rounding floor = kRound); + static int64_t time_to_timestamp(const double& time, const rational& timebase, Rounding floor = kRound); static int64_t rescale_timestamp(const int64_t& ts, const rational& source, const rational& dest); static int64_t rescale_timestamp_ceil(const int64_t& ts, const rational& source, const rational& dest); diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index 83d65e9f7..3f0f08829 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -199,30 +199,7 @@ void TimeRangeList::insert(TimeRange range_to_add) void TimeRangeList::remove(const TimeRange &remove) { - int sz = this->size(); - - for (int i=0;i remove.in()) { - // This element's out point overlaps the range's in, we'll trim it - compare.set_out(remove.in()); - } else if (compare.in() < remove.out() && compare.out() > remove.out()) { - // This element's in point overlaps the range's out, we'll trim it - compare.set_in(remove.out()); - } - } + util_remove(&array_, remove); } bool TimeRangeList::contains(const TimeRange &range, bool in_inclusive, bool out_inclusive) const @@ -296,6 +273,78 @@ uint qHash(const TimeRange &r, uint seed) return qHash(r.in(), seed) ^ qHash(r.out(), seed); } +TimeRangeListFrameIterator::TimeRangeListFrameIterator() : + TimeRangeListFrameIterator(TimeRangeList(), rational::NaN) +{ +} + +TimeRangeListFrameIterator::TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase) : + list_(list), + timebase_(timebase), + index_(-1), + size_(-1) +{ + UpdateIndexIfNecessary(); +} + +bool TimeRangeListFrameIterator::GetNext(rational *out) +{ + if (!HasNext()) { + return false; + } + + // Output current value + *out = current_; + + // Determine next value by adding timebase + current_ += timebase_; + + // If this time is outside the current range, jump to the next one + UpdateIndexIfNecessary(); + + return true; +} + +bool TimeRangeListFrameIterator::HasNext() const +{ + return index_ < list_.size(); +} + +int TimeRangeListFrameIterator::size() +{ + if (size_ == -1) { + // Size isn't calculated automatically for optimization, so we'll calculate it now + size_ = 0; + + foreach (const TimeRange &range, list_) { + rational start = Timecode::snap_time_to_timebase(range.in(), timebase_, Timecode::kCeil); + rational end = Timecode::snap_time_to_timebase(range.out(), timebase_, Timecode::kFloor); + + if (end == range.out()) { + end -= timebase_; + } + + int64_t start_ts = Timecode::time_to_timestamp(start, timebase_); + int64_t end_ts = Timecode::time_to_timestamp(end, timebase_); + + size_ += 1 + (end_ts - start_ts); + } + } + + return size_; +} + +void TimeRangeListFrameIterator::UpdateIndexIfNecessary() +{ + while (index_ < list_.size() && (index_ == -1 || current_ >= list_.at(index_).out())) { + index_++; + + if (index_ < list_.size()) { + current_ = Timecode::snap_time_to_timebase(list_.at(index_).in(), timebase_, Timecode::kCeil); + } + } +} + } QDebug operator<<(QDebug debug, const olive::TimeRange &r) diff --git a/app/common/timerange.h b/app/common/timerange.h index 28913f6df..7362f6518 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -22,6 +22,7 @@ #define TIMERANGE_H #include "rational.h" +#include "timecodefunctions.h" namespace olive { @@ -80,6 +81,36 @@ public: void remove(const TimeRange& remove); + template + static void util_remove(QVector *list, const TimeRange &remove) + { + int sz = list->size(); + + for (int i=0;iremoveAt(i); + i--; + sz--; + } else if (compare.Contains(remove, false, false)) { + // The remove range is within this element, only choice is to split the element into two + T new_range = compare; + new_range.set_in(remove.out()); + compare.set_out(remove.in()); + list->append(new_range); + break; + } else if (compare.in() < remove.in() && compare.out() > remove.in()) { + // This element's out point overlaps the range's in, we'll trim it + compare.set_out(remove.in()); + } else if (compare.in() < remove.out() && compare.out() > remove.out()) { + // This element's in point overlaps the range's out, we'll trim it + compare.set_in(remove.out()); + } + } + } + bool contains(const TimeRange& range, bool in_inclusive = true, bool out_inclusive = true) const; bool isEmpty() const @@ -127,16 +158,69 @@ public: return array_.last(); } + const TimeRange& at(int index) const + { + return array_.at(index); + } + const QVector& internal_array() const { return array_; } + bool operator==(const TimeRangeList &rhs) const + { + return array_ == rhs.array_; + } + private: QVector array_; }; +class TimeRangeListFrameIterator +{ +public: + TimeRangeListFrameIterator(); + TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase); + + bool GetNext(rational *out); + + bool HasNext() const; + + QVector ToVector() const + { + TimeRangeListFrameIterator copy(list_, timebase_); + QVector times; + rational r; + while (copy.GetNext(&r)) { + times.append(r); + } + return times; + } + + int size(); + + void reset() + { + *this = TimeRangeListFrameIterator(); + } + +private: + void UpdateIndexIfNecessary(); + + TimeRangeList list_; + + rational timebase_; + + rational current_; + + int index_; + + int size_; + +}; + uint qHash(const TimeRange& r, uint seed = 0); } diff --git a/app/core.cpp b/app/core.cpp index 9dcb7f96c..a4075b0a0 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -75,14 +75,15 @@ namespace olive { Core* Core::instance_ = nullptr; -const uint Core::kProjectVersion = 210122; +const uint Core::kProjectVersion = 210528; Core::Core(const CoreParams& params) : main_window_(nullptr), tool_(Tool::kPointer), addable_object_(Tool::kAddableEmpty), snapping_(true), - core_params_(params) + core_params_(params), + effects_slider_is_being_dragged_(false) { // Store reference to this object, making the assumption that Core will only ever be made in // main(). This will obviously break if not. @@ -421,6 +422,7 @@ void Core::CreateNewSequence() command->add_child(new NodeAddCommand(active_project, new_sequence)); command->add_child(new FolderAddChild(GetSelectedFolderInActiveProject(), new_sequence)); + command->add_child(new NodeSetPositionCommand(new_sequence, new_sequence, QPointF(0, 0), false)); // Create and connect default nodes to new sequence new_sequence->add_default_nodes(command); diff --git a/app/core.h b/app/core.h index 67fb6a95d..83658d163 100644 --- a/app/core.h +++ b/app/core.h @@ -304,6 +304,10 @@ public: void OpenNodeInViewer(ViewerOutput* viewer); + bool EffectsSliderIsBeingDragged() const {return effects_slider_is_being_dragged_;} + + void SetEffectsSliderIsBeingDragged(bool e) {effects_slider_is_being_dragged_ = e;} + static const uint kProjectVersion; public slots: @@ -571,6 +575,11 @@ private: */ QVector autorecovered_projects_; + /** + * @brief An effects slider somewhere is being dragged + */ + bool effects_slider_is_being_dragged_; + private slots: void SaveAutorecovery(); diff --git a/app/dialog/sequence/sequence.h b/app/dialog/sequence/sequence.h index 9e3ac5362..ad9036be4 100644 --- a/app/dialog/sequence/sequence.h +++ b/app/dialog/sequence/sequence.h @@ -115,6 +115,7 @@ private: virtual Project* GetRelevantProject() const override; + protected: virtual void redo() override; virtual void undo() override; diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 47fecec22..646736ccf 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -22,6 +22,7 @@ #include +#include "core.h" #include "node/output/track/track.h" #include "transition/transition.h" #include "widget/slider/floatslider.h" @@ -29,6 +30,8 @@ namespace olive { +#define super Node + const QString Block::kLengthInput = QStringLiteral("length_in"); const QString Block::kMediaInInput = QStringLiteral("media_in_in"); const QString Block::kEnabledInput = QStringLiteral("enabled_in"); @@ -47,7 +50,6 @@ Block::Block() : SetInputProperty(kLengthInput, QStringLiteral("min"), QVariant::fromValue(rational(0, 1))); SetInputProperty(kLengthInput, QStringLiteral("view"), RationalSlider::kTime); SetInputProperty(kLengthInput, QStringLiteral("viewlock"), true); - IgnoreInvalidationsFrom(kLengthInput); IgnoreHashingFrom(kLengthInput); AddInput(kMediaInInput, NodeValue::kRational, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); @@ -221,7 +223,7 @@ void Block::set_length_internal(const rational &length) void Block::Retranslate() { - Node::Retranslate(); + super::Retranslate(); SetInputName(kLengthInput, tr("Length")); SetInputName(kMediaInInput, tr("Media In")); @@ -235,4 +237,24 @@ void Block::Hash(const QString &, QCryptographicHash &, const rational &, const // A block does nothing by default, so we hash nothing } +void Block::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) +{ + TimeRange r; + + if (from == kLengthInput) { + // We must intercept the signal here + r = TimeRange(qMin(length(), last_length_), RATIONAL_MAX); + + if (!Core::instance()->EffectsSliderIsBeingDragged()) { + last_length_ = length(); + } + + options.insert(QStringLiteral("lengthevent"), true); + } else { + r = range; + } + + super::InvalidateCache(r, from, element, options); +} + } diff --git a/app/node/block/block.h b/app/node/block/block.h index f8c6ab8ed..7593b1a81 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -155,6 +155,8 @@ public: virtual void Hash(const QString& output, QCryptographicHash &hash, const rational &time, const VideoParams& video_params) const override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element = -1, InvalidateCacheOptions options = InvalidateCacheOptions()) override; + static const QString kLengthInput; static const QString kMediaInInput; static const QString kEnabledInput; @@ -193,6 +195,8 @@ private: QVector block_links_; + rational last_length_; + }; } diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index fe8cdc6e2..7db010721 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -53,7 +53,7 @@ QString ClipBlock::Description() const return tr("A time-based node that represents a media source."); } -void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) +void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) { Q_UNUSED(element) @@ -63,10 +63,10 @@ void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int rational start = MediaToSequenceTime(range.in()); rational end = MediaToSequenceTime(range.out()); - super::InvalidateCache(TimeRange(start, end), from, element, job_time); + super::InvalidateCache(TimeRange(start, end), from, element, options); } else { // Otherwise, pass signal along normally - super::InvalidateCache(range, from, element, job_time); + super::InvalidateCache(range, from, element, options); } } diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index ef542526f..c81d3b510 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -42,7 +42,7 @@ public: virtual QString id() const override; virtual QString Description() const override; - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) override; virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; diff --git a/app/node/graph.cpp b/app/node/graph.cpp index 972310906..26bfe6937 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -44,6 +44,45 @@ void NodeGraph::Clear() } } +qreal NodeGraph::GetNodeContextHeight(Node *context) +{ + const PositionMap &map = position_map_.value(context); + + qreal top = 0, bottom = 0; + + foreach (const QPointF &pt, map) { + top = qMin(pt.y(), top); + bottom = qMax(pt.y(), bottom); + } + + return bottom - top; +} + +int NodeGraph::GetNumberOfContextsNodeIsIn(Node *node) const +{ + int count = 0; + + for (auto it=position_map_.cbegin(); it!=position_map_.cend(); it++) { + if (it.value().contains(node)) { + count++; + } + } + + return count; +} + +bool NodeGraph::NodeOutputsToContext(Node *node) const +{ + for (auto it=position_map_.cbegin(); it!=position_map_.cend(); it++) { + const PositionMap &pm = it.value(); + if (pm.contains(node) && node->OutputsTo(it.key(), true)) { + return true; + } + } + + return false; +} + void NodeGraph::childEvent(QChildEvent *event) { super::childEvent(event); @@ -75,6 +114,18 @@ void NodeGraph::childEvent(QChildEvent *event) emit NodeRemoved(node); emit node->RemovedFromGraph(this); + for (auto it=position_map_.begin(); it!=position_map_.end(); it++) { + PositionMap &map = it.value(); + for (auto jt=map.begin(); jt!=map.end(); ) { + if (jt.key() == node) { + jt = map.erase(jt); + emit NodePositionRemoved(node, it.key()); + } else { + jt++; + } + } + } + } } } diff --git a/app/node/graph.h b/app/node/graph.h index ecb1073a9..dbdfb990b 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -63,6 +63,55 @@ public: return default_nodes_; } + bool NodeMapContainsNode(Node* node, Node* context) const + { + return position_map_.value(context).contains(node); + } + + QPointF GetNodePosition(Node* node, Node* context) + { + return position_map_.value(context).value(node); + } + + void SetNodePosition(Node* node, Node* context, const QPointF& pos) + { + position_map_[context].insert(node, pos); + emit NodePositionAdded(node, context, pos); + } + + void RemoveNodePosition(Node* node, Node* context) + { + PositionMap& map = position_map_[context]; + map.remove(node); + if (map.isEmpty()) { + position_map_.remove(context); + } + emit NodePositionRemoved(node, context); + } + + bool ContextContainsNode(Node *node, Node *context) + { + return position_map_[context].contains(node); + } + + qreal GetNodeContextHeight(Node *context); + + using PositionMap = QMap; + + const PositionMap &GetNodesForContext(Node *context) + { + return position_map_[context]; + } + + const QMap &GetPositionMap() const + { + return position_map_; + } + + int GetNumberOfContextsNodeIsIn(Node *node) const; + + bool NodeOutputsToContext(Node *node) const; + signals: /** * @brief Signal emitted when a Node is added to the graph @@ -80,6 +129,10 @@ signals: void ValueChanged(const NodeInput& input); + void NodePositionAdded(Node *node, Node *relative, const QPointF &position); + + void NodePositionRemoved(Node *node, Node *relative); + protected: void AddDefaultNode(Node* n) { @@ -93,6 +146,10 @@ private: QVector default_nodes_; + QMap position_map_; + + PositionMap root_position_map_; + }; } diff --git a/app/node/node.cpp b/app/node/node.cpp index 044d9c263..23f022641 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -46,7 +46,6 @@ const QString Node::kDefaultOutput = QStringLiteral("output"); Node::Node(bool create_default_output) : can_be_deleted_(true), override_color_(-1), - last_change_time_(0), folder_(nullptr), operation_stack_(0), cache_result_(false) @@ -91,20 +90,6 @@ void Node::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, uint versi LoadInput(reader, xml_node_data, cancelled); } else if (reader->name() == QStringLiteral("ptr")) { xml_node_data.node_ptrs.insert(reader->readElementText().toULongLong(), this); - } else if (reader->name() == QStringLiteral("pos")) { - QPointF p; - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("x")) { - p.setX(reader->readElementText().toDouble()); - } else if (reader->name() == QStringLiteral("y")) { - p.setY(reader->readElementText().toDouble()); - } else { - reader->skipCurrentElement(); - } - } - - SetPosition(p); } else if (reader->name() == QStringLiteral("label")) { SetLabel(reader->readElementText()); } else if (reader->name() == QStringLiteral("color")) { @@ -166,11 +151,6 @@ void Node::Save(QXmlStreamWriter *writer) const { writer->writeTextElement(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); - writer->writeStartElement(QStringLiteral("pos")); - writer->writeTextElement(QStringLiteral("x"), QString::number(GetPosition().x())); - writer->writeTextElement(QStringLiteral("y"), QString::number(GetPosition().y())); - writer->writeEndElement(); // pos - writer->writeTextElement(QStringLiteral("label"), GetLabel()); writer->writeTextElement(QStringLiteral("color"), QString::number(override_color_)); @@ -282,9 +262,6 @@ void Node::ConnectEdge(const NodeOutput &output, const NodeInput &input) input.node()->input_connections_[input] = output; output.node()->output_connections_.push_back(std::pair({output, input})); - // Update change times - input.node()->UpdateLastChangedTime(); - // Call internal events input.node()->InputConnectedEvent(input.input(), input.element(), output); output.node()->OutputConnectedEvent(output.output(), input); @@ -314,9 +291,6 @@ void Node::DisconnectEdge(const NodeOutput &output, const NodeInput &input) OutputConnections& outputs = output.node()->output_connections_; outputs.erase(std::find(outputs.begin(), outputs.end(), std::pair({output, input}))); - // Update change times - input.node()->UpdateLastChangedTime(); - // Call internal events input.node()->InputDisconnectedEvent(input.input(), input.element(), output); output.node()->OutputDisconnectedEvent(output.output(), input); @@ -953,7 +927,7 @@ void Node::InputArrayResize(const QString &id, int size, bool undoable) if (undoable) { Core::instance()->undo_stack()->push(c); } else { - c->redo(); + c->redo_now(); delete c; } } @@ -1045,12 +1019,12 @@ NodeValueTable Node::Value(const QString& output, NodeValueDatabase &value) cons return value.Merge(); } -void Node::InvalidateCache(const TimeRange &range, const QString &from, int element, qint64 job_time) +void Node::InvalidateCache(const TimeRange &range, const QString &from, int element, InvalidateCacheOptions options) { Q_UNUSED(from) Q_UNUSED(element) - SendInvalidateCache(range, job_time); + SendInvalidateCache(range, options); } void Node::BeginOperation() @@ -1134,7 +1108,7 @@ void Node::CopyDependencyGraph(const QVector &src, const QVector } } -Node *Node::CopyNodeAndDependencyGraphMinusItemsInternal(QMap& created, const Node *node, MultiUndoCommand *command) +Node *Node::CopyNodeAndDependencyGraphMinusItemsInternal(QMap& created, Node *node, MultiUndoCommand *command) { // Make a new node of the same type Node* copy = node->copy(); @@ -1171,17 +1145,26 @@ Node *Node::CopyNodeAndDependencyGraphMinusItemsInternal(QMapparent()->GetPositionMap().contains(node)) { + // This node is a context, copy the context + const NodeGraph::PositionMap &map = node->parent()->GetPositionMap().value(node); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + // Add either the copy (if it exists) or the original node to the context + command->add_child(new NodeSetPositionCommand(created.value(it.key(), it.key()), copy, it.value(), false)); + } + } + return copy; } -Node *Node::CopyNodeAndDependencyGraphMinusItems(const Node *node, MultiUndoCommand *command) +Node *Node::CopyNodeAndDependencyGraphMinusItems(Node *node, MultiUndoCommand *command) { - QMap created; + QMap created; return CopyNodeAndDependencyGraphMinusItemsInternal(created, node, command); } -Node *Node::CopyNodeInGraph(const Node *node, MultiUndoCommand *command) +Node *Node::CopyNodeInGraph(Node *node, MultiUndoCommand *command) { Node* copy; @@ -1194,19 +1177,28 @@ Node *Node::CopyNodeInGraph(const Node *node, MultiUndoCommand *command) copy)); command->add_child(new NodeCopyInputsCommand(node, copy, true)); + + if (node->parent()->GetPositionMap().contains(node)) { + // This node is a context, copy the context + const NodeGraph::PositionMap &map = node->parent()->GetPositionMap().value(node); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + // Add to the context + command->add_child(new NodeSetPositionCommand(it.key(), copy, it.value(), false)); + } + } } return copy; } -void Node::SendInvalidateCache(const TimeRange &range, qint64 job_time) +void Node::SendInvalidateCache(const TimeRange &range, const InvalidateCacheOptions &options) { if (GetOperationStack() == 0) { for (const OutputConnection& conn : output_connections_) { // Send clear cache signal to the Node const NodeInput& in = conn.second; - in.node()->InvalidateCache(range, in.input(), in.element(), job_time); + in.node()->InvalidateCache(range, in.input(), in.element(), options); } } } @@ -1482,7 +1474,6 @@ void Node::CopyInputs(const Node *source, Node *destination, bool include_connec CopyInput(source, destination, input, include_connections, true); } - destination->SetPosition(source->GetPosition()); destination->SetLabel(source->GetLabel()); destination->SetOverrideColor(source->GetOverrideColor()); } @@ -1642,15 +1633,28 @@ void Node::GenerateFrame(FramePtr frame, const GenerateJob &job) const Q_UNUSED(job) } -bool Node::OutputsTo(Node *n, bool recursively) const +bool Node::OutputsTo(Node *n, bool recursively, const OutputConnections &ignore_edges, const OutputConnection &added_edge) const { for (const OutputConnection& conn : output_connections_) { + if (std::find(ignore_edges.cbegin(), ignore_edges.cend(), conn) != ignore_edges.cend()) { + // If this edge is in the "ignore edges" list, skip it + continue; + } + Node* connected = conn.second.node(); if (connected == n) { return true; - } else if (recursively && connected->OutputsTo(n, recursively)) { + } else if (recursively && connected->OutputsTo(n, recursively, ignore_edges, added_edge)) { return true; + } else if (added_edge.first.node() == this) { + Node *proposed_connected = added_edge.second.node(); + + if (proposed_connected == n) { + return true; + } else if (recursively && proposed_connected->OutputsTo(n, recursively, ignore_edges, added_edge)) { + return true; + } } } @@ -1717,7 +1721,7 @@ bool Node::InputsFrom(const QString &id, bool recursively) const return false; } -int Node::GetRoutesTo(Node *n) const +int Node::GetNumberOfRoutesTo(Node *n) const { bool outputs_directly = false; int routes = 0; @@ -1728,7 +1732,7 @@ int Node::GetRoutesTo(Node *n) const if (connected_node == n) { outputs_directly = true; } else { - routes += connected_node->GetRoutesTo(n); + routes += connected_node->GetNumberOfRoutesTo(n); } } @@ -1831,33 +1835,8 @@ QVariant Node::PtrToValue(void *ptr) return reinterpret_cast(ptr); } -const QPointF &Node::GetPosition() const -{ - return position_; -} - -void Node::SetPosition(const QPointF &pos, bool move_dependencies_relatively_too) -{ - QPointF old_pos = position_; - - position_ = pos; - - emit PositionChanged(position_); - - if (move_dependencies_relatively_too) { - QPointF difference = pos - old_pos; - - for (auto it=input_connections_.cbegin(); it!=input_connections_.cend(); it++) { - Node* c = it->second.node(); - c->SetPosition(c->GetPosition() + difference, true); - } - } -} - void Node::ParameterValueChanged(const QString& input, int element, const TimeRange& range) { - UpdateLastChangedTime(); - InputValueChangedEvent(input, element); emit ValueChanged(NodeInput(this, input, element), range); @@ -2049,11 +2028,6 @@ void Node::SaveImmediate(QXmlStreamWriter *writer, const QString& input, int ele } } -void Node::UpdateLastChangedTime() -{ - last_change_time_ = QDateTime::currentMSecsSinceEpoch(); -} - TimeRange Node::GetRangeAffectedByKeyframe(NodeKeyframe *key) const { const NodeKeyframeTrack& key_track = GetTrackFromKeyframe(key); @@ -2302,36 +2276,144 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() { if (commands_.isEmpty()) { // Move first node - NodeSetPositionCommand* set_pos_command = new NodeSetPositionCommand(node_, position_, move_dependencies_); - set_pos_command->redo(); + NodeSetPositionCommand* set_pos_command = new NodeSetPositionCommand(node_, relative_, position_, move_dependencies_); + set_pos_command->redo_now(); commands_.append(set_pos_command); // Get bounding rect - QRectF bounding_rect(position_.x() - 0.5, position_.y() - 0.5, 1, 1); + qreal bounding_rect_sz = 1.0; + qreal bounding_rect_half_sz = bounding_rect_sz * 0.5; + QRectF bounding_rect(position_.x() - bounding_rect_half_sz, position_.y() - bounding_rect_half_sz, bounding_rect_sz, bounding_rect_sz); // Start moving other nodes foreach (Node* surrounding, node_->parent()->nodes()) { - if (bounding_rect.contains(surrounding->GetPosition()) && surrounding != node_) { - QPointF new_pos = surrounding->GetPosition(); + if (surrounding != node_) { + QPointF surrounding_position = node_->parent()->GetNodePosition(surrounding, relative_); + if (bounding_rect.contains(surrounding_position)) { + QPointF new_pos = surrounding_position; - qreal move_rate = 0.50; + qreal move_rate = 0.50; - if (surrounding->GetPosition().y() < position_.y()) { - move_rate = -move_rate; + if (surrounding_position.y() < position_.y()) { + move_rate = -move_rate; + } + + new_pos.setY(new_pos.y() + move_rate); + + auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, relative_, new_pos, true); + sur_command->redo(); + commands_.append(sur_command); } - - new_pos.setY(new_pos.y() + move_rate); - - auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, new_pos, true); - sur_command->redo(); - commands_.append(sur_command); } } } else { for (int i=0; iredo(); + commands_.at(i)->redo_now(); } } } +void NodeSetPositionCommand::redo() +{ + graph_ = node_->parent(); + if (!(added_ = !graph_->NodeMapContainsNode(node_, relevant_))) { + old_pos_ = graph_->GetNodePosition(node_, relevant_); + } + graph_->SetNodePosition(node_, relevant_, pos_); +} + +void NodeSetPositionCommand::undo() +{ + if (added_) { + graph_->RemoveNodePosition(node_, relevant_); + } else { + graph_->SetNodePosition(node_, relevant_, old_pos_); + } +} + +void NodeSetPositionAsChildCommand::redo() +{ + if (!sub_command_) { + // Calculate position of node + NodeGraph *graph = parent_->parent(); + QPointF pos = graph->GetNodePosition(parent_, relative_); + + // This is a dependency, so we'll place it one X before + pos.setX(pos.x() - 1); + + // The Y will be calculated using the index and child count + pos.setY(pos.y() - (double(child_count_)*0.5) + this_index_ + 0.5); + + sub_command_ = new MultiUndoCommand(); + if (shift_surroundings_) { + sub_command_->add_child(new NodeSetPositionAndShiftSurroundingsCommand(node_, relative_, pos, true)); + } else { + sub_command_->add_child(new NodeSetPositionCommand(node_, relative_, pos, true)); + } + } + + sub_command_->redo(); +} + +void NodeSetPositionToOffsetOfAnotherNodeCommand::redo() +{ + NodeGraph *graph = node_->parent(); + old_pos_ = graph->GetNodePosition(node_, relative_); + graph->SetNodePosition(node_, relative_, graph->GetNodePosition(other_node_, relative_) + offset_); +} + +void NodeSetPositionToOffsetOfAnotherNodeCommand::undo() +{ + NodeGraph *graph = node_->parent(); + graph->SetNodePosition(node_, relative_, old_pos_); +} + +void NodeRemovePositionFromContextCommand::redo() +{ + NodeGraph *graph = node_->parent(); + + contained_ = graph->ContextContainsNode(node_, context_); + + if (contained_) { + old_pos_ = graph->GetNodePosition(node_, context_); + graph->RemoveNodePosition(node_, context_); + } +} + +void NodeRemovePositionFromContextCommand::undo() +{ + if (contained_) { + NodeGraph *graph = node_->parent(); + graph->SetNodePosition(node_, context_, old_pos_); + } +} + +void NodeRemovePositionFromAllContextsCommand::redo() +{ + NodeGraph *graph = node_->parent(); + + if (points_.empty()) { + // No points yet, let's see what points we should remove + auto map = graph->GetPositionMap(); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + if (it.value().contains(node_)) { + points_.insert({it.key(), it.value().value(node_)}); + } + } + } + + for (auto it=points_.cbegin(); it!=points_.cend(); it++) { + graph->RemoveNodePosition(node_, it->first); + } +} + +void NodeRemovePositionFromAllContextsCommand::undo() +{ + NodeGraph *graph = node_->parent(); + + for (auto it=points_.crbegin(); it!=points_.crend(); it++) { + graph->SetNodePosition(node_, it->first, it->second); + } +} + } diff --git a/app/node/node.h b/app/node/node.h index d4ad010fc..31c144d8f 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -557,7 +558,8 @@ public: * Whether to keep traversing down outputs to find this node (TRUE) or stick to immediate outputs * (FALSE). */ - bool OutputsTo(Node* n, bool recursively) const; + bool OutputsTo(Node* n, bool recursively, const OutputConnections &ignore_edges = OutputConnections(), const OutputConnection &added_edge = OutputConnection()) const; + /** * @brief Same as OutputsTo(Node*), but for a node ID rather than a specific instance. */ @@ -581,7 +583,7 @@ public: /** * @brief Determines how many paths go from this node out to another node */ - int GetRoutesTo(Node* n) const; + int GetNumberOfRoutesTo(Node* n) const; /** * @brief Severs all input and output connections @@ -621,6 +623,8 @@ public: */ static T* ValueToPtr(const QVariant& ptr); + using InvalidateCacheOptions = QHash; + /** * @brief Signal all dependent Nodes that anything cached between start_range and end_range is now invalid and * requires re-rendering @@ -630,16 +634,11 @@ public: * the DAG. Even if the time needs to be transformed somehow (e.g. converting media time to sequence time), you can * call this function with transformed time and relay the signal that way. */ - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time); + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element = -1, InvalidateCacheOptions options = InvalidateCacheOptions()); - void InvalidateCache(const TimeRange& range, const QString& from, int element = -1) + void InvalidateCache(const TimeRange& range, const NodeInput& from, const InvalidateCacheOptions &options = InvalidateCacheOptions()) { - InvalidateCache(range, from, element, last_change_time_); - } - - void InvalidateCache(const TimeRange& range, const NodeInput& from) - { - InvalidateCache(range, from.input(), from.element()); + InvalidateCache(range, from.input(), from.element(), options); } /** @@ -689,9 +688,9 @@ public: static QVector CopyDependencyGraph(const QVector& nodes, MultiUndoCommand *command); static void CopyDependencyGraph(const QVector& src, const QVector& dst, MultiUndoCommand *command); - static Node* CopyNodeAndDependencyGraphMinusItems(const Node* node, MultiUndoCommand* command); + static Node* CopyNodeAndDependencyGraphMinusItems(Node* node, MultiUndoCommand* command); - static Node* CopyNodeInGraph(const Node* node, MultiUndoCommand* command); + static Node* CopyNodeInGraph(Node *node, MultiUndoCommand* command); /** * @brief Return whether this Node can be deleted or not @@ -718,10 +717,6 @@ public: */ virtual NodeValueTable Value(const QString &output, NodeValueDatabase& value) const; - const QPointF& GetPosition() const; - - void SetPosition(const QPointF& pos, bool move_dependencies_relatively_too = false); - virtual bool HasGizmos() const; virtual void DrawGizmos(NodeValueDatabase& db, QPainter* p); @@ -889,7 +884,7 @@ protected: SetInputProperty(id, QStringLiteral("combo_str"), strings); } - void SendInvalidateCache(const TimeRange &range, qint64 job_time); + void SendInvalidateCache(const TimeRange &range, const InvalidateCacheOptions &options); /** * @brief Don't send cache invalidation signals if `input` is connected or disconnected @@ -1014,6 +1009,7 @@ private: virtual Project* GetRelevantProject() const override; + protected: virtual void redo() override { node_->InputArrayInsert(input_, index_, false); @@ -1040,6 +1036,9 @@ private: size_(size) {} + virtual Project* GetRelevantProject() const override; + + protected: virtual void redo() override { old_size_ = node_->InputArraySize(input_); @@ -1072,8 +1071,6 @@ private: node_->ArrayResizeInternal(input_, old_size_); } - virtual Project* GetRelevantProject() const override; - private: Node* node_; QString input_; @@ -1130,7 +1127,7 @@ private: void ArrayResizeInternal(const QString& id, int size); - static Node *CopyNodeAndDependencyGraphMinusItemsInternal(QMap& created, const Node *node, MultiUndoCommand *command); + static Node *CopyNodeAndDependencyGraphMinusItemsInternal(QMap &created, Node *node, MultiUndoCommand *command); /** * @brief Immediates aren't deleted, so the actual array size may be larger than ArraySize() @@ -1157,8 +1154,6 @@ private: void SaveImmediate(QXmlStreamWriter *writer, const QString &input, int element) const; - void UpdateLastChangedTime(); - /** * @brief Intelligently determine how what time range is affected by a keyframe */ @@ -1180,11 +1175,6 @@ private: */ bool can_be_deleted_; - /** - * @brief UI position for NodeViews - */ - QPointF position_; - /** * @brief Custom user label for node */ @@ -1213,8 +1203,6 @@ private: OutputConnections output_connections_; - qint64 last_change_time_; - QString tooltip_; Folder* folder_; @@ -1312,44 +1300,41 @@ using NodePtr = std::shared_ptr; class NodeSetPositionCommand : public UndoCommand { public: - NodeSetPositionCommand(Node* node, const QPointF& position, bool move_dependencies_relatively) : - node_(node), - new_pos_(position), - move_deps_(move_dependencies_relatively) + NodeSetPositionCommand(Node* node, Node* relevant, const QPointF& pos, bool move_dependencies_relatively) { + node_ = node; + relevant_ = relevant; + pos_ = pos; + move_deps_ = move_dependencies_relatively; } - virtual Project * GetRelevantProject() const override + virtual Project* GetRelevantProject() const override { return node_->project(); } - virtual void redo() override - { - old_pos_ = node_->GetPosition(); - node_->SetPosition(new_pos_, move_deps_); - } +protected: + virtual void redo() override; - virtual void undo() override - { - node_->SetPosition(old_pos_, move_deps_); - } + virtual void undo() override; private: Node* node_; - - QPointF new_pos_; + Node* relevant_; + QPointF pos_; QPointF old_pos_; - + bool added_; bool move_deps_; + NodeGraph *graph_; }; class NodeSetPositionAndShiftSurroundingsCommand : public UndoCommand { public: - NodeSetPositionAndShiftSurroundingsCommand(Node* node, const QPointF& pos, bool move_dependencies_relatively) : + NodeSetPositionAndShiftSurroundingsCommand(Node* node, Node *relative, const QPointF& pos, bool move_dependencies_relatively) : node_(node), + relative_(relative), position_(pos), move_dependencies_(move_dependencies_relatively) {} @@ -1364,18 +1349,21 @@ public: return node_->project(); } +protected: virtual void redo() override; virtual void undo() override { for (int i=commands_.size()-1; i>=0; i--) { - commands_.at(i)->undo(); + commands_.at(i)->undo_now(); } } private: Node* node_; + Node *relative_; + QPointF position_; bool move_dependencies_; @@ -1387,9 +1375,10 @@ private: class NodeSetPositionAsChildCommand : public UndoCommand { public: - NodeSetPositionAsChildCommand(Node* node, Node* parent, int this_index, int child_count, bool shift_surroundings) : + NodeSetPositionAsChildCommand(Node* node, Node* parent, Node *relative, double this_index, int child_count, bool shift_surroundings) : node_(node), parent_(parent), + relative_(relative), this_index_(this_index), child_count_(child_count), shift_surroundings_(shift_surroundings), @@ -1407,27 +1396,8 @@ public: return node_->project(); } - virtual void redo() override - { - if (!sub_command_) { - // Calculate position of node - QPointF pos = parent_->GetPosition(); - - // This is a dependency, so we'll place it one X before - pos.setX(pos.x() - 1); - - // The Y will be calculated using the index and child count - pos.setY(pos.y() - (double(child_count_)*0.5) + this_index_ + 0.5); - - if (shift_surroundings_) { - sub_command_ = new NodeSetPositionAndShiftSurroundingsCommand(node_, pos, true); - } else { - sub_command_ = new NodeSetPositionCommand(node_, pos, true); - } - } - - sub_command_->redo(); - } +protected: + virtual void redo() override; virtual void undo() override { @@ -1437,22 +1407,44 @@ public: private: Node* node_; Node* parent_; + Node *relative_; - int this_index_; + double this_index_; int child_count_; bool shift_surroundings_; - UndoCommand* sub_command_; + MultiUndoCommand* sub_command_; + +}; + +class NodePositionCloseChildGapCommand : public UndoCommand +{ +public: + NodePositionCloseChildGapCommand(Node *parent, void *relative, int remove_index, int child_count, bool shift_surroundings); + + virtual Project * GetRelevantProject() const override + { + return parent_->project(); + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Node *parent_; }; class NodeSetPositionToOffsetOfAnotherNodeCommand : public UndoCommand { public: - NodeSetPositionToOffsetOfAnotherNodeCommand(Node* node, Node* other_node, const QPointF& offset) : + NodeSetPositionToOffsetOfAnotherNodeCommand(Node* node, Node* other_node, Node *relative, const QPointF& offset) : node_(node), other_node_(other_node), + relative_(relative), offset_(offset) {} @@ -1461,20 +1453,72 @@ public: return node_->project(); } - virtual void redo() override - { - node_->SetPosition(other_node_->GetPosition() + offset_); - } +protected: + virtual void redo() override; - virtual void undo() override - { - node_->SetPosition(other_node_->GetPosition() - offset_); - } + virtual void undo() override; private: Node* node_; Node* other_node_; + Node *relative_; QPointF offset_; + QPointF old_pos_; + +}; + +class NodeRemovePositionFromContextCommand : public UndoCommand +{ +public: + NodeRemovePositionFromContextCommand(Node *node, Node *context) : + node_(node), + context_(context) + { + } + + virtual Project * GetRelevantProject() const override + { + return node_->project(); + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + Node *context_; + + QPointF old_pos_; + + bool contained_; + +}; + +class NodeRemovePositionFromAllContextsCommand : public UndoCommand +{ +public: + NodeRemovePositionFromAllContextsCommand(Node *node) : + node_(node) + { + } + + virtual Project * GetRelevantProject() const override + { + return node_->project(); + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + std::map points_; }; diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 6aebb508c..8b689afea 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -40,9 +40,6 @@ const QString Track::kMutedInput = QStringLiteral("muted_in"); Track::Track() : track_type_(Track::kNone), - track_length_(0), - midop_track_length_(0), - preop_track_length_(0), index_(-1), locked_(false) { @@ -280,11 +277,8 @@ void Track::InputDisconnectedEvent(const QString &input, int element, const Node // Update lengths if (next) { UpdateInOutFrom(blocks_.indexOf(next)); - } else if (blocks_.isEmpty()) { - SetLengthInternal(0); - } else { - SetLengthInternal(blocks_.last()->out()); } + emit TrackLengthChanged(); disconnect(b, &Block::LengthChanged, this, &Track::BlockLengthChanged); @@ -431,7 +425,7 @@ QVector Track::BlocksAtTimeRange(const TimeRange &range) const return list; } -void Track::InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) +void Track::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) { if (GetOperationStack() != 0) { return; @@ -443,7 +437,8 @@ void Track::InvalidateCache(const TimeRange& range, const QString& from, int ele if (from == kBlockInput && element >= 0 - && (b = dynamic_cast(GetConnectedOutput(from, element).node()))) { + && (b = dynamic_cast(GetConnectedOutput(from, element).node())) + && !options.value(QStringLiteral("lengthevent")).toBool()) { // Limit the range signal to the corresponding block if (range.out() <= b->in() || range.in() >= b->out()) { return; @@ -451,11 +446,14 @@ void Track::InvalidateCache(const TimeRange& range, const QString& from, int ele limited = TimeRange(qMax(range.in(), b->in()), qMin(range.out(), b->out())); } else { - limited = TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), qMax(preop_track_length_, track_length()))); - preop_track_length_ = track_length_; + limited = range; } - Node::InvalidateCache(limited, from, element, job_time); + // NOTE: For now, I figure we drop this key, but we may find in the future that it's advantageous + // to keep it + options.remove(QStringLiteral("lengthevent")); + + Node::InvalidateCache(limited, from, element, options); } void Track::InsertBlockBefore(Block* block, Block* after) @@ -520,7 +518,7 @@ void Track::AppendBlock(Block *block) EndOperation(); // Invalidate area that block was added to - Node::InvalidateCache(TimeRange(block->in(), track_length()), kBlockInput); + Node::InvalidateCache(TimeRange(block->in(), block->out()), kBlockInput); } void Track::RippleRemoveBlock(Block *block) @@ -552,13 +550,17 @@ void Track::ReplaceBlock(Block *old, Block *replace) if (old->length() == replace->length()) { Node::InvalidateCache(TimeRange(replace->in(), replace->out()), kBlockInput); } else { - Node::InvalidateCache(TimeRange(replace->in(), RATIONAL_MAX), kBlockInput); + Node::InvalidateCache(TimeRange(replace->in(), track_length()), kBlockInput); } } -const rational &Track::track_length() const +rational Track::track_length() const { - return track_length_; + if (blocks_.isEmpty()) { + return 0; + } else { + return blocks_.last()->out(); + } } QString Track::GetDefaultTrackName(Track::Type type, int index) @@ -600,15 +602,6 @@ void Track::Hash(const QString &output, QCryptographicHash &hash, const rational } } -void Track::EndOperation() -{ - super::EndOperation(); - - if (track_length_ != midop_track_length_) { - SetLengthInternal(midop_track_length_); - } -} - void Track::SetMuted(bool e) { SetStandardValue(kMutedInput, e); @@ -640,7 +633,7 @@ void Track::UpdateInOutFrom(int index) emit BlocksRefreshed(); // Update track length - SetLengthInternal(last_out); + emit TrackLengthChanged(); } int Track::GetArrayIndexFromBlock(Block *block) const @@ -658,48 +651,12 @@ int Track::GetCacheIndexFromArrayIndex(int index) const return block_array_indexes_.indexOf(index); } -void Track::SetLengthInternal(const rational &r, bool invalidate) -{ - // Hold track length until operation stack is empty - midop_track_length_ = r; - - if (GetOperationStack() == 0 && track_length_ != r) { - TimeRange invalidate_range(track_length_, r); - track_length_ = r; - preop_track_length_ = qMax(preop_track_length_, track_length_); - emit TrackLengthChanged(); - - if (invalidate) { - Node::InvalidateCache(invalidate_range, kBlockInput); - } - } -} - void Track::BlockLengthChanged() { // Assumes sender is a Block Block* b = static_cast(sender()); - rational old_out = b->out(); - UpdateInOutFrom(blocks_.indexOf(b)); - - rational new_out = b->out(); - - TimeRange invalidate_region(qMin(old_out, new_out), track_length()); - - // The cache won't start while dragging, so we store up our invalidations if it's held down - // and release them once the mouse is no longer pressed - if (qApp->mouseButtons() & Qt::LeftButton) { - block_length_pending_invalidations_.insert(invalidate_region); - } else if (!block_length_pending_invalidations_.isEmpty()) { - foreach (const TimeRange& r, block_length_pending_invalidations_) { - Node::InvalidateCache(r, kBlockInput); - } - block_length_pending_invalidations_.clear(); - } - - Node::InvalidateCache(invalidate_region, kBlockInput); } uint qHash(const Track::Reference &r, uint seed) diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 94b745963..b69801384 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -286,7 +286,7 @@ public: return blocks_; } - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) override; /** * @brief Adds Block `block` at the very beginning of the Sequence before all other clips @@ -330,7 +330,7 @@ public: */ void ReplaceBlock(Block* old, Block* replace); - const rational& track_length() const; + rational track_length() const; static QString GetDefaultTrackName(Track::Type type, int index); @@ -345,8 +345,6 @@ public: return waveform_; } - virtual void EndOperation() override; - static const double kTrackHeightDefault; static const double kTrackHeightMinimum; static const double kTrackHeightInterval; @@ -420,8 +418,6 @@ private: int GetCacheIndexFromArrayIndex(int index) const; - void SetLengthInternal(const rational& r, bool invalidate = true); - TimeRangeList block_length_pending_invalidations_; QVector blocks_; @@ -429,12 +425,6 @@ private: Track::Type track_type_; - rational track_length_; - - rational midop_track_length_; - - rational preop_track_length_; - double track_height_; int index_; diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 072086693..910480576 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -222,7 +222,7 @@ void ViewerOutput::ShiftCache(const rational &from, const rational &to) ShiftAudioCache(from, to); } -void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) +void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) { Q_UNUSED(element) @@ -233,16 +233,16 @@ void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, if (invalidated_range.in() != invalidated_range.out()) { if (from == kTextureInput || from == kVideoParamsInput) { - video_frame_cache_.Invalidate(invalidated_range, job_time); + video_frame_cache_.Invalidate(invalidated_range); } else { - audio_playback_cache_.Invalidate(invalidated_range, job_time); + audio_playback_cache_.Invalidate(invalidated_range); } } } VerifyLength(); - super::InvalidateCache(range, from, element, job_time); + super::InvalidateCache(range, from, element, options); } QVector ViewerOutput::inputs_for_output(const QString &output) const @@ -300,14 +300,8 @@ void ViewerOutput::Retranslate() void ViewerOutput::VerifyLength() { video_length_ = VerifyLengthInternal(Track::kVideo); - if (video_cache_enabled_) { - video_frame_cache_.SetLength(video_length_); - } audio_length_ = VerifyLengthInternal(Track::kAudio); - if (audio_cache_enabled_) { - audio_playback_cache_.SetLength(audio_length_); - } rational subtitle_length = VerifyLengthInternal(Track::kSubtitle); diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 4ac02eaa7..df5fda49d 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -66,7 +66,7 @@ public: void ShiftAudioCache(const rational& from, const rational& to); void ShiftCache(const rational& from, const rational& to); - virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, qint64 job_time) override; + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) override; virtual QVector inputs_for_output(const QString& output) const override; @@ -154,6 +154,9 @@ public: virtual NodeOutput GetConnectedSampleOutput(); + void SetViewerVideoCacheEnabled(bool e) { video_cache_enabled_ = e; } + void SetViewerAudioCacheEnabled(bool e) { audio_cache_enabled_ = e; } + static const QString kVideoParamsInput; static const QString kAudioParamsInput; @@ -202,9 +205,6 @@ protected: int AddStream(Track::Type type, const QVariant &value); - void SetViewerVideoCacheEnabled(bool e) { video_cache_enabled_ = e; } - void SetViewerAudioCacheEnabled(bool e) { audio_cache_enabled_ = e; } - private: rational last_length_; rational video_length_; diff --git a/app/node/project/folder/folder.cpp b/app/node/project/folder/folder.cpp index 9508425f2..91fda055f 100644 --- a/app/node/project/folder/folder.cpp +++ b/app/node/project/folder/folder.cpp @@ -138,18 +138,17 @@ void FolderAddChild::redo() Node::ConnectEdge(child_, NodeInput(folder_, Folder::kChildInput, array_index)); if (autoposition_) { - old_position_ = child_->GetPosition(); if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(child_, folder_, array_index, array_index+1, true); + position_command_ = new NodeSetPositionAsChildCommand(child_, folder_, folder_->project()->root(), array_index, array_index+1, true); } - position_command_->redo(); + position_command_->redo_now(); } } void FolderAddChild::undo() { if (position_command_) { - position_command_->undo(); + position_command_->undo_now(); } Node::DisconnectEdge(child_, NodeInput(folder_, Folder::kChildInput, folder_->InputArraySize(Folder::kChildInput)-1)); diff --git a/app/node/project/folder/folder.h b/app/node/project/folder/folder.h index 6d0521433..ce71033b6 100644 --- a/app/node/project/folder/folder.h +++ b/app/node/project/folder/folder.h @@ -135,6 +135,7 @@ public: return folder_->project(); } + protected: virtual void redo() override; virtual void undo() override @@ -209,6 +210,7 @@ public: virtual Project * GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -220,8 +222,6 @@ private: bool autoposition_; - QPointF old_position_; - NodeSetPositionAsChildCommand* position_command_; }; diff --git a/app/node/project/project.cpp b/app/node/project/project.cpp index ab4717e7c..5cc50d274 100644 --- a/app/node/project/project.cpp +++ b/app/node/project/project.cpp @@ -39,26 +39,27 @@ Project::Project() : // Generate UUID for this project RegenerateUuid(); + // Folder root for project + root_ = new Folder(); + root_->setParent(this); + root_->SetLabel(tr("Root")); + root_->SetCanBeDeleted(false); + SetNodePosition(root_, root_, QPointF(0, 0)); + // Adds a color manager "node" to this project so that it synchronizes color_manager_ = new ColorManager(); color_manager_->setParent(this); - color_manager_->SetPosition(QPointF(1, 0)); + SetNodePosition(color_manager_, root_, QPointF(1, 0)); color_manager_->SetCanBeDeleted(false); AddDefaultNode(color_manager_); // Same with project settings settings_ = new ProjectSettingsNode(); settings_->setParent(this); - settings_->SetPosition(QPointF(2, 0)); + SetNodePosition(settings_, root_, QPointF(2, 0)); settings_->SetCanBeDeleted(false); AddDefaultNode(settings_); - // Folder root for project - root_ = new Folder(); - root_->setParent(this); - root_->SetLabel(tr("Root")); - root_->SetCanBeDeleted(false); - connect(color_manager(), &ColorManager::ValueChanged, this, &Project::ColorManagerValueChanged); } @@ -135,6 +136,71 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, uint } } + } else if (reader->name() == QStringLiteral("positions")) { + + while (XMLReadNextStartElement(reader)) { + + if (reader->name() == QStringLiteral("context")) { + + quintptr context_ptr = 0; + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + context_ptr = attr.value().toULongLong(); + break; + } + } + + Node *context = xml_node_data.node_ptrs.value(context_ptr); + + if (!context) { + qWarning() << "Failed to find pointer for context"; + reader->skipCurrentElement(); + } else { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("node")) { + quintptr node_ptr = 0; + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + node_ptr = attr.value().toULongLong(); + break; + } + } + + Node *node = xml_node_data.node_ptrs.value(node_ptr); + + if (!node) { + qWarning() << "Failed to find pointer for node position"; + reader->skipCurrentElement(); + } else { + QPointF pos; + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("x")) { + pos.setX(reader->readElementText().toDouble()); + } else if (reader->name() == QStringLiteral("y")) { + pos.setY(reader->readElementText().toDouble()); + } else { + reader->skipCurrentElement(); + } + } + + SetNodePosition(node, context, pos); + } + + } else { + reader->skipCurrentElement(); + } + } + } + + } else { + + reader->skipCurrentElement(); + + } + + } + } else { // Skip this @@ -176,6 +242,32 @@ void Project::Save(QXmlStreamWriter *writer) const writer->writeEndElement(); // nodes + writer->writeStartElement(QStringLiteral("positions")); + + for (auto it=GetPositionMap().cbegin(); it!=GetPositionMap().cend(); it++) { + writer->writeStartElement(QStringLiteral("context")); + + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(it.key()))); + + const PositionMap &map = it.value(); + + for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { + writer->writeStartElement(QStringLiteral("node")); + + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(jt.key()))); + + const QPointF &pos = jt.value(); + writer->writeTextElement(QStringLiteral("x"), QString::number(pos.x())); + writer->writeTextElement(QStringLiteral("y"), QString::number(pos.y())); + + writer->writeEndElement(); // node + } + + writer->writeEndElement(); // context + } + + writer->writeEndElement(); // positions + // Save main window project layout MainWindowLayoutInfo main_window_info = Core::instance()->main_window()->SaveLayout(); main_window_info.toXml(writer); diff --git a/app/node/project/sequence/sequence.cpp b/app/node/project/sequence/sequence.cpp index 6ee484adc..5773e42c5 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 { @@ -63,8 +64,8 @@ void Sequence::add_default_nodes(MultiUndoCommand* command) command->add_child(video_track_command); command->add_child(audio_track_command); } else { - video_track_command->redo(); - audio_track_command->redo(); + video_track_command->redo_now(); + audio_track_command->redo_now(); delete video_track_command; delete audio_track_command; } diff --git a/app/panel/node/node.cpp b/app/panel/node/node.cpp index 18e207918..4b7334378 100644 --- a/app/panel/node/node.cpp +++ b/app/panel/node/node.cpp @@ -20,20 +20,39 @@ #include "node.h" +#include + namespace olive { NodePanel::NodePanel(QWidget *parent) : PanelWidget(QStringLiteral("NodePanel"), parent) { + QWidget *outer_widget = new QWidget(this); + + QVBoxLayout *outer_layout = new QVBoxLayout(outer_widget); + outer_layout->setMargin(0); + + NodeViewToolBar *toolbar = new NodeViewToolBar(); + outer_layout->addWidget(toolbar); + // Create NodeView widget node_view_ = new NodeView(this); + outer_layout->addWidget(node_view_); + + // Connect toolbar to NodeView + connect(toolbar, &NodeViewToolBar::MiniMapEnabledToggled, node_view_, &NodeView::SetMiniMapEnabled); + connect(toolbar, &NodeViewToolBar::AddNodeClicked, node_view_, &NodeView::ShowAddMenu); + + // Set defaults + toolbar->SetMiniMapEnabled(true); + node_view_->SetMiniMapEnabled(true); // Connect node view signals to this panel connect(node_view_, &NodeView::NodesSelected, this, &NodePanel::NodesSelected); connect(node_view_, &NodeView::NodesDeselected, this, &NodePanel::NodesDeselected); // Set it as the main widget of this panel - SetWidgetWithPadding(node_view_); + SetWidgetWithPadding(outer_widget); // Set strings Retranslate(); diff --git a/app/panel/node/node.h b/app/panel/node/node.h index 999626b7c..607671055 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -22,6 +22,7 @@ #define NODEPANEL_H #include "widget/nodeview/nodeview.h" +#include "widget/nodeview/nodeviewtoolbar.h" #include "widget/panel/panel.h" namespace olive { @@ -40,9 +41,14 @@ public: return node_view_->GetGraph(); } - void SetGraph(NodeGraph *graph) + void SetGraph(NodeGraph *graph, const QVector &nodes) { - node_view_->SetGraph(graph); + node_view_->SetGraph(graph, nodes); + } + + void ClearGraph() + { + node_view_->ClearGraph(); } virtual void SelectAll() override @@ -96,28 +102,14 @@ public: } public slots: - void Select(const QVector& nodes) + void Select(const QVector& nodes, bool center_view_on_item) { - node_view_->Select(nodes); + node_view_->Select(nodes, center_view_on_item); } - void SelectWithDependencies(const QVector& nodes) + void SelectWithDependencies(const QVector& nodes, bool center_view_on_item) { - node_view_->SelectWithDependencies(nodes); - } - - void SelectBlocks(const QVector& blocks) - { - QVector nodes(blocks.size()); - memcpy(nodes.data(), blocks.constData(), blocks.size() * sizeof(Block*)); - node_view_->SelectWithDependencies(nodes); - } - - void DeselectBlocks(const QVector& nodes) - { - Q_UNUSED(nodes) - qDebug() << "Stub"; - //node_view_->DeselectBlocks(nodes); + node_view_->SelectWithDependencies(nodes, center_view_on_item); } signals: diff --git a/app/panel/panelmanager.cpp b/app/panel/panelmanager.cpp index ce06e7f9c..779c2be35 100644 --- a/app/panel/panelmanager.cpp +++ b/app/panel/panelmanager.cpp @@ -28,8 +28,7 @@ PanelManager* PanelManager::instance_ = nullptr; PanelManager::PanelManager(QObject *parent) : QObject(parent), - locked_(false), - last_focused_panel_(nullptr) + locked_(false) { } @@ -46,11 +45,11 @@ const QList &PanelManager::panels() return focus_history_; } -PanelWidget *PanelManager::CurrentlyFocused() const +PanelWidget *PanelManager::CurrentlyFocused(bool enable_hover) const { // If hover focus is enabled, find the currently hovered panel and return it (if no panel is hovered, resort to // default behavior) - if (Config::Current()["HoverFocus"].toBool()) { + if (enable_hover && Config::Current()[QStringLiteral("HoverFocus")].toBool()) { PanelWidget* hovered = CurrentlyHovered(); if (hovered != nullptr) { @@ -111,7 +110,7 @@ void PanelManager::FocusChanged(QWidget *old, QWidget *now) if (panel_cast_test) { - if (last_focused_panel_ != panel_cast_test) { + if (focus_history_.first() != panel_cast_test) { // If so, bump this to the top of the focus history int panel_index = focus_history_.indexOf(panel_cast_test); @@ -130,7 +129,6 @@ void PanelManager::FocusChanged(QWidget *old, QWidget *now) focus_history_.move(panel_index, 0); } - last_focused_panel_ = panel_cast_test; emit FocusedPanelChanged(panel_cast_test); } @@ -158,10 +156,6 @@ void PanelManager::PanelDestroyed() PanelWidget* panel = static_cast(sender()); focus_history_.removeOne(panel); - - if (last_focused_panel_ == panel) { - last_focused_panel_ = focus_history_.isEmpty() ? nullptr : focus_history_.first(); - } } } diff --git a/app/panel/panelmanager.h b/app/panel/panelmanager.h index 1f9c68dee..50dfefce6 100644 --- a/app/panel/panelmanager.h +++ b/app/panel/panelmanager.h @@ -65,14 +65,12 @@ public: /** * @brief Return the currently focused widget, or nullptr if nothing is focused * - * This result == CurrentlyFocused() if HoverFocus is true + * This result == CurrentlyFocused() if HoverFocus is true and panel is hovered */ - PanelWidget* CurrentlyFocused() const; + PanelWidget* CurrentlyFocused(bool enable_hover = true) const; /** * @brief Return the widget that the mouse is currently hovering over, or nullptr if nothing is hovered over - * - * This result == CurrentlyFocused() if HoverFocus is true */ PanelWidget* CurrentlyHovered() const; @@ -155,13 +153,6 @@ private: */ static PanelManager* instance_; - /** - * @brief The last panel that was focused - * - * Stored to prevent emitting FocusedPanelChanged() multiple times for the same panel - */ - PanelWidget* last_focused_panel_; - private slots: /** * @brief Processing if a panel gets deleted @@ -195,6 +186,12 @@ T *PanelManager::CreatePanel(QWidget *parent) // Connect destroy signal so we can remove it from focus history connect(panel, &PanelWidget::destroyed, this, &PanelManager::PanelDestroyed, Qt::DirectConnection); + if (focus_history_.size() == 1) { + // This is the first panel, focus it + panel->SetBorderVisible(true); + emit FocusedPanelChanged(panel); + } + return panel; } diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index e0f6fcfa3..dd0b2ced9 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -57,6 +57,7 @@ ProjectPanel::ProjectPanel(QWidget *parent) : explorer_ = new ProjectExplorer(this); layout->addWidget(explorer_); connect(explorer_, &ProjectExplorer::DoubleClickedItem, this, &ProjectPanel::ItemDoubleClickSlot); + connect(explorer_, &ProjectExplorer::SelectionChanged, this, &ProjectPanel::SelectionChanged); // Set toolbar's view to the explorer's view toolbar->SetView(explorer_->view_type()); diff --git a/app/panel/project/project.h b/app/panel/project/project.h index 2fb36ce8c..bfa23c455 100644 --- a/app/panel/project/project.h +++ b/app/panel/project/project.h @@ -66,6 +66,8 @@ public slots: signals: void ProjectNameChanged(); + void SelectionChanged(const QVector &selected); + private: virtual void Retranslate() override; diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 44b1f3fba..390de75cd 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -33,13 +33,7 @@ TimelinePanel::TimelinePanel(QWidget *parent) : Retranslate(); - connect(tw, &TimelineWidget::BlocksSelected, this, &TimelinePanel::BlocksSelected); - connect(tw, &TimelineWidget::BlocksDeselected, this, &TimelinePanel::BlocksDeselected); -} - -void TimelinePanel::Clear() -{ - static_cast(GetTimeBasedWidget())->Clear(); + connect(tw, &TimelineWidget::BlockSelectionChanged, this, &TimelinePanel::BlockSelectionChanged); } void TimelinePanel::SplitAtPlayhead() diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 5a195b818..5b853dce3 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -35,8 +35,6 @@ class TimelinePanel : public TimeBasedPanel public: TimelinePanel(QWidget* parent); - void Clear(); - void SplitAtPlayhead(); QByteArray SaveSplitterState() const; @@ -89,13 +87,16 @@ public: void OverwriteFootageAtPlayhead(const QVector &footage); + const QVector& GetSelectedBlocks() const + { + return static_cast(GetTimeBasedWidget())->GetSelectedBlocks(); + } + protected: virtual void Retranslate() override; signals: - void BlocksSelected(const QVector& selected_blocks); - - void BlocksDeselected(const QVector& deselected_blocks); + void BlockSelectionChanged(const QVector& selected_blocks); }; diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index 6f6898154..122aa5422 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -46,6 +46,8 @@ set(OLIVE_SOURCES render/rendercache.h render/rendererthreadwrapper.cpp render/rendererthreadwrapper.h + render/renderjobtracker.cpp + render/renderjobtracker.h render/rendermanager.cpp render/rendermanager.h render/rendermodes.h diff --git a/app/render/audioparams.cpp b/app/render/audioparams.cpp index 062cf5be5..1654ef084 100644 --- a/app/render/audioparams.cpp +++ b/app/render/audioparams.cpp @@ -54,13 +54,6 @@ const QVector AudioParams::kSupportedChannelLayouts = { const AudioParams::Format AudioParams::kInternalFormat = AudioParams::kFormatFloat32; -qint64 AudioParams::time_to_bytes(const double &time) const -{ - Q_ASSERT(is_valid()); - - return qint64(time_to_samples(time)) * channel_count() * bytes_per_sample_per_channel(); -} - bool AudioParams::operator==(const AudioParams &other) const { return (format() == other.format() @@ -94,11 +87,28 @@ QAudioFormat::SampleType AudioParams::GetQtSampleType(AudioParams::Format format return QAudioFormat::Unknown; } +qint64 AudioParams::time_to_bytes(const double &time) const +{ + return time_to_bytes_per_channel(time) * channel_count(); +} + qint64 AudioParams::time_to_bytes(const rational &time) const { return time_to_bytes(time.toDouble()); } +qint64 AudioParams::time_to_bytes_per_channel(const double &time) const +{ + Q_ASSERT(is_valid()); + + return qint64(time_to_samples(time)) * bytes_per_sample_per_channel(); +} + +qint64 AudioParams::time_to_bytes_per_channel(const rational &time) const +{ + return time_to_bytes_per_channel(time.toDouble()); +} + qint64 AudioParams::time_to_samples(const double &time) const { Q_ASSERT(is_valid()); @@ -139,6 +149,13 @@ rational AudioParams::bytes_to_time(const qint64 &bytes) const return samples_to_time(bytes_to_samples(bytes)); } +rational AudioParams::bytes_per_channel_to_time(const qint64 &bytes) const +{ + Q_ASSERT(is_valid()); + + return samples_to_time(bytes_to_samples(bytes * channel_count())); +} + int AudioParams::channel_count() const { return channel_count_; diff --git a/app/render/audioparams.h b/app/render/audioparams.h index b79c14ede..d28105c40 100644 --- a/app/render/audioparams.h +++ b/app/render/audioparams.h @@ -161,12 +161,15 @@ public: qint64 time_to_bytes(const double& time) const; qint64 time_to_bytes(const rational& time) const; + qint64 time_to_bytes_per_channel(const double& time) const; + qint64 time_to_bytes_per_channel(const rational& time) const; qint64 time_to_samples(const double& time) const; qint64 time_to_samples(const rational& time) const; qint64 samples_to_bytes(const qint64& samples) const; rational samples_to_time(const qint64& samples) const; qint64 bytes_to_samples(const qint64 &bytes) const; rational bytes_to_time(const qint64 &bytes) const; + rational bytes_per_channel_to_time(const qint64 &bytes) const; int channel_count() const; int bytes_per_sample_per_channel() const; int bits_per_sample() const; diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index c29f03ed2..0f9a1183a 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -22,13 +22,14 @@ #include #include +#include #include #include "common/filefunctions.h" namespace olive { -const qint64 AudioPlaybackCache::kDefaultSegmentSize = 5242880; +const qint64 AudioPlaybackCache::kDefaultSegmentSizePerChannel = 10 * 1024 * 1024; AudioPlaybackCache::AudioPlaybackCache(QObject* parent) : PlaybackCache(parent) @@ -56,79 +57,81 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) emit ParametersChanged(); } -void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const qint64 &job_time) +void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples, const AudioVisualWaveform *waveform) { - QList valid_ranges = GetValidRanges(range, job_time); - if (valid_ranges.isEmpty()) { - return; - } - // Ensure if we have enough segments to write this data, creating more if not - qint64 length_diff = params_.time_to_bytes(range.out()) - playlist_.GetLength(); + qint64 length_diff = params_.time_to_bytes_per_channel(range.out()) - playlist_.GetLength(); while (length_diff > 0) { - qint64 seg_sz = qMin(kDefaultSegmentSize, length_diff); + qint64 seg_sz = qMin(kDefaultSegmentSizePerChannel, length_diff); playlist_.push_back(CreateSegment(seg_sz, playlist_.GetLength())); length_diff -= seg_sz; } - // Convert to packed data, which is what we store on disk so it can be played back easily - QByteArray a; - if (samples) { - a = samples->toPackedData(); - } - // Keep track of validated ranges so we can signal them all at once at the end TimeRangeList ranges_we_validated; + // Calculate buffer size per channel + qint64 buffer_size_per_channel = samples->sample_count() * params_.bytes_per_sample_per_channel(); + // Write each valid range to the segments foreach (const TimeRange& r, valid_ranges) { rational this_segment_in = 0; // Write PCM to playlist for (auto it=playlist_.begin(); it!=playlist_.end(); it++) { - rational this_segment_out = this_segment_in + params_.bytes_to_time((*it).size()); + rational this_segment_out = this_segment_in + params_.bytes_per_channel_to_time((*it).size()); if (r.in() < this_segment_out) { // We'll write at least something to this segment - QFile seg_file((*it).filename()); + bool succeeded = true; - if (seg_file.open(QFile::ReadWrite)) { - // Calculate how much to write - rational this_write_in_point = qMax(r.in(), this_segment_in); - rational this_write_out_point = qMin(r.out(), this_segment_out); + // Calculate how much to write + rational this_write_in_point = qMax(r.in(), this_segment_in); + rational this_write_out_point = qMin(r.out(), this_segment_out); - // Calculate what the byte offsets are going to be in this segment file - rational in_point_relative = this_write_in_point - this_segment_in; - qint64 dst_offset = params_.time_to_bytes(in_point_relative); + for (int i=0; i<(*it).channels(); i++) { + QFile seg_file((*it).filename(i)); - // Calculate where to retrieve data from in the source buffer - qint64 src_offset = params_.time_to_bytes(this_write_in_point - range.in()); + if (seg_file.open(QFile::ReadWrite)) { + // Calculate what the byte offsets are going to be in this segment file + rational in_point_relative = this_write_in_point - this_segment_in; + qint64 dst_offset = params_.time_to_bytes_per_channel(in_point_relative); - // Determine how many bytes need to be written - qint64 total_write_length = params_.time_to_bytes(this_write_out_point - this_write_in_point); + // Calculate where to retrieve data from in the source buffer + qint64 src_offset = params_.time_to_bytes_per_channel(this_write_in_point - range.in()); - // Determine how many bytes we actually have in the source buffer - qint64 possible_write_length = qMin(qMax(qint64(0), a.size() - src_offset), total_write_length); + // Determine how many bytes need to be written + qint64 total_write_length = params_.time_to_bytes_per_channel(this_write_out_point - this_write_in_point); - // Seek to our start offset - seg_file.seek(dst_offset); + // Retrieve data buffer + const char *a = reinterpret_cast(samples->data(i)); - // If we have source bytes to write, write them here - if (possible_write_length > 0) { - seg_file.write(a.data() + src_offset, possible_write_length); + // Determine how many bytes we actually have in the source buffer + qint64 possible_write_length = qMin(qMax(qint64(0), buffer_size_per_channel - src_offset), total_write_length); + + // Seek to our start offset + seg_file.seek(dst_offset); + + // If we have source bytes to write, write them here + if (possible_write_length > 0) { + seg_file.write(a + src_offset, possible_write_length); + } + + if (possible_write_length < total_write_length) { + // Fill remaining space with silence + QByteArray s(total_write_length - possible_write_length, 0x00); + seg_file.write(s); + } + + seg_file.close(); + } else { + qWarning() << "Failed to write PCM data to" << seg_file.fileName(); + succeeded = false; } + } - if (possible_write_length < total_write_length) { - // Fill remaining space with silence - QByteArray s(total_write_length - possible_write_length, 0x00); - seg_file.write(s); - } - - seg_file.close(); - + if (succeeded) { ranges_we_validated.insert(TimeRange(this_write_in_point, this_write_out_point)); - } else { - qWarning() << "Failed to write PCM data to" << seg_file.fileName(); } } @@ -154,23 +157,22 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample } } -void AudioPlaybackCache::WriteSilence(const TimeRange &range, qint64 job_time) +void AudioPlaybackCache::WriteSilence(const TimeRange &range) { // WritePCM will automatically fill non-existent bytes with silence, so we just have to send // it an empty sample buffer - WritePCM(range, nullptr, nullptr, job_time); + WritePCM(range, {range}, nullptr, nullptr); } void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational &to_in_time) { - if (from_in_time == to_in_time || from_in_time >= GetLength()) { - // Nothing to be done + qint64 to = params_.time_to_bytes_per_channel(to_in_time); + qint64 from = params_.time_to_bytes_per_channel(from_in_time); + + if (from >= playlist_.GetLength()) { return; } - qint64 to = params_.time_to_bytes(to_in_time); - qint64 from = params_.time_to_bytes(from_in_time); - int to_seg_index = playlist_.GetIndexOfPosition(to); int from_seg_index = playlist_.GetIndexOfPosition(from); @@ -202,7 +204,7 @@ void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational qint64 time_to_insert = to - from; while (time_to_insert) { - qint64 new_seg_sz = qMin(kDefaultSegmentSize, time_to_insert); + qint64 new_seg_sz = qMin(kDefaultSegmentSizePerChannel, time_to_insert); // Set offset to 0 for now and fill it in later playlist_.insert(insert_index, CreateSegment(new_seg_sz, 0)); @@ -262,56 +264,45 @@ void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational } } -void AudioPlaybackCache::LengthChangedEvent(const rational& old, const rational& newlen) -{ - Q_UNUSED(old) - - if (!params_.is_valid()) { - return; - } - - qint64 new_len_in_bytes = params_.time_to_bytes(newlen); - - while (new_len_in_bytes < playlist_.GetLength()) { - Segment& last_seg = playlist_.back(); - - if (playlist_.GetLength() - last_seg.size() < new_len_in_bytes) { - // Truncate this segment rather than removing it - qint64 diff = playlist_.GetLength() - new_len_in_bytes; - - TrimSegmentOut(&last_seg, last_seg.size() - diff); - } else { - // Remove last segment - RemoveSegmentFromArray(playlist_.size() - 1); - } - } -} - AudioPlaybackCache::Segment AudioPlaybackCache::CloneSegment(const AudioPlaybackCache::Segment &s) const { Segment new_seg = s; - // Copy data to a new file - QString new_filename = GenerateSegmentFilename(); - QFile::copy(s.filename(), new_filename); + new_seg.set_channels(s.channels()); - new_seg.set_filename(new_filename); + // Copy data to a new file + for (int i=0; igenerate(); new_seg_filename = QDir(GetCacheDirectory()).filePath(QStringLiteral("%1.pcm").arg(r)); } while (QFileInfo::exists(new_seg_filename)); @@ -330,21 +321,23 @@ QString AudioPlaybackCache::GenerateSegmentFilename() const void AudioPlaybackCache::TrimSegmentIn(AudioPlaybackCache::Segment *s, qint64 new_length) { // Read filename - QFile f(s->filename()); - if (f.open(QFile::ReadWrite)) { - // Read segment into memory, according to the size we acknowledge - QByteArray data = f.read(s->size()); + for (int i=0; ichannels(); i++) { + QFile f(s->filename(i)); + if (f.open(QFile::ReadWrite)) { + // Read segment into memory, according to the size we acknowledge + QByteArray data = f.read(s->size()); - // Trim to new length - data = data.right(new_length); + // Trim to new length + data = data.right(new_length); - // Seek to start and write - f.seek(0); + // Seek to start and write + f.seek(0); - // Write trimmed data - f.write(data); + // Write trimmed data + f.write(data); - f.close(); + f.close(); + } } s->set_size(new_length); @@ -358,14 +351,19 @@ void AudioPlaybackCache::TrimSegmentOut(AudioPlaybackCache::Segment *s, qint64 n void AudioPlaybackCache::RemoveSegmentFromArray(int index) { - QFile::remove(playlist_.at(index).filename()); + const Segment &s = playlist_.at(index); + for (int i=0; i AudioPlaybackCache::GetValidRanges(const TimeRange& range, const qint64& job_time) -{ - QList valid_ranges; - - for (int i=jobs_.size()-1;i>=0;i--) { - const JobIdentifier& job = jobs_.at(i); - - if (job_time >= job.job_time && job.range.OverlapsWith(range)) { - valid_ranges.append(job.range.Intersected(range)); - } - } - - return valid_ranges; -} - AudioPlaybackCache::PlaybackDevice *AudioPlaybackCache::CreatePlaybackDevice(QObject* parent) const { - return new PlaybackDevice(playlist_, parent); + return new PlaybackDevice(playlist_, params_.bytes_per_sample_per_channel(), parent); } -AudioPlaybackCache::Segment::Segment(qint64 size, const QString &filename) +AudioPlaybackCache::Segment::Segment(qint64 size) { size_ = size; - filename_ = filename; } -AudioPlaybackCache::PlaybackDevice::PlaybackDevice(const AudioPlaybackCache::Playlist &playlist, QObject *parent) : +AudioPlaybackCache::PlaybackDevice::PlaybackDevice(const AudioPlaybackCache::Playlist &playlist, int sample_sz, QObject *parent) : QIODevice(parent), playlist_(playlist), current_segment_(0), - segment_read_index_(0) + segment_read_index_(0), + sample_size_(sample_sz) { } @@ -458,37 +441,66 @@ qint64 AudioPlaybackCache::PlaybackDevice::readData(char *data, qint64 maxSize) && current_segment_ < playlist_.size()) { const Segment& cs = playlist_.at(current_segment_); qint64 current_segment_sz = cs.size(); - QFile segment_file(cs.filename()); - if (segment_file.open(QFile::ReadOnly)) { - // Seek to our stored index of this segment - segment_file.seek(segment_read_index_); + QVector segment_files(cs.channels()); + segment_files.fill(nullptr); - // Determine how many bytes to read - qint64 this_read_length = qMin(current_segment_sz - segment_read_index_, - maxSize - read_size); + bool all_files_opened = true; - // Read those bytes - segment_file.read(data + read_size, this_read_length); + // Open all file handles + for (int i=0; iopen(QFile::ReadOnly)) { + // Seek to our stored index of this segment + f->seek(segment_read_index_); + } else { + all_files_opened = false; + break; + } + } - // Add to the read index - segment_read_index_ += this_read_length; + // If all file handles opened successfully, time to interleave and send them out + if (all_files_opened) { + // Determine how many bytes to read + qint64 this_read_length = qMin((current_segment_sz - segment_read_index_) * cs.channels(), maxSize - read_size); - // Add to the read size - read_size += this_read_length; + qint64 target = read_size + this_read_length; - // If we've reached the end of this segment, tick the counter over to the next segment - if (segment_read_index_ == current_segment_sz) { - // Jump to the next file - segment_read_index_ = 0; - current_segment_++; + while (read_size < target) { + for (int i=0; iread(data + read_size, sample_size_); + + // Add to the read size + read_size += sample_size_; + } + + // Add to the read index + segment_read_index_ += sample_size_; + + // If we've reached the end of this segment, tick the counter over to the next segment + if (segment_read_index_ == current_segment_sz) { + // Jump to the next file + segment_read_index_ = 0; + current_segment_++; + } + } + } + + // Close and delete file handles + for (int i=0; iisOpen()) { + f->close(); + } + delete f; } - } else { - qWarning() << "Failed to read data from segment"; - break; } } diff --git a/app/render/audioplaybackcache.h b/app/render/audioplaybackcache.h index eb61643ee..4e4bc76de 100644 --- a/app/render/audioplaybackcache.h +++ b/app/render/audioplaybackcache.h @@ -66,17 +66,14 @@ public: void SetParameters(const AudioParams& params); - void WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const qint64& job_time); + void WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, SampleBufferPtr samples, const AudioVisualWaveform *waveform); - void WriteSilence(const TimeRange &range, qint64 job_time); - - QList GetValidRanges(const TimeRange &range, const qint64 &job_time); + void WriteSilence(const TimeRange &range); class Segment { public: - Segment() = default; - Segment(qint64 size, const QString& filename); + Segment(qint64 size = 0); qint64 size() const { @@ -98,14 +95,24 @@ public: offset_ = o; } - const QString& filename() const + int channels() const { - return filename_; + return filenames_.size(); } - void set_filename(const QString& filename) + void set_channels(int index) { - filename_ = filename; + filenames_.resize(index); + } + + const QString& filename(int index) const + { + return filenames_.at(index); + } + + void set_filename(int index, const QString& filename) + { + filenames_[index] = filename; } qint64 end() const @@ -114,7 +121,7 @@ public: } private: - QString filename_; + QVector filenames_; qint64 size_; @@ -136,7 +143,7 @@ public: class PlaybackDevice : public QIODevice { public: - PlaybackDevice(const Playlist& playlist, QObject* parent = nullptr); + PlaybackDevice(const Playlist& playlist, int sample_sz, QObject* parent = nullptr); virtual ~PlaybackDevice() override; @@ -169,6 +176,8 @@ public: qint64 segment_read_index_; + int sample_size_; + }; /** @@ -193,10 +202,8 @@ signals: protected: virtual void ShiftEvent(const rational& from, const rational& to) override; - virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; - private: - static const qint64 kDefaultSegmentSize; + static const qint64 kDefaultSegmentSizePerChannel; Segment CloneSegment(const Segment& s) const; diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index e78578488..5f0b9a921 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -32,7 +32,6 @@ #include "codec/frame.h" #include "common/filefunctions.h" -#include "common/timecodefunctions.h" #include "render/diskmanager.h" namespace olive { @@ -52,24 +51,32 @@ FrameHashCache::FrameHashCache(QObject *parent) : } } -QByteArray FrameHashCache::GetHash(const rational &time) +QByteArray FrameHashCache::GetHash(const int64_t &time) { - return time_hash_map_.value(time); + if (time < GetMapSize()) { + return time_hash_map_.at(time); + } else { + return QByteArray(); + } } -void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const qint64& job_time, bool frame_exists) +QByteArray FrameHashCache::GetHash(const rational &time) { - for (int i=jobs_.size()-1; i>=0; i--) { - const JobIdentifier& job = jobs_.at(i); + return GetHash(ToTimestamp(time)); +} - if (job.range.Contains(time) - && job_time < job.job_time) { - // Hash here has changed since this frame started rendering, discard it - return; - } +void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, bool frame_exists) +{ + int64_t ts = ToTimestamp(time); + if (ts >= GetMapSize()) { + // Disabled: bizarrely causes the whole app to hang indefinitely when used + // Reserve an extra minute to cut down on the amount of reallocations to make + //time_hash_map_.reserve(ts + timebase_.flipped().toDouble() * 60); + + // Add enough entries to insert this hash + time_hash_map_.resize(ts + 1); } - - time_hash_map_.insert(time, hash); + time_hash_map_[ts] = hash; TimeRange validated_range; if (frame_exists) { @@ -85,11 +92,11 @@ void FrameHashCache::SetTimebase(const rational &tb) void FrameHashCache::ValidateFramesWithHash(const QByteArray &hash) { - const TimeRangeList& invalidated_ranges = GetInvalidatedRanges(); + auto invalidated_ranges = GetInvalidatedRanges(ToTime(GetMapSize())); - for (auto iterator=time_hash_map_.begin();iterator!=time_hash_map_.end();iterator++) { - if (iterator.value() == hash) { - TimeRange frame_range(iterator.key(), iterator.key() + timebase_); + for (int64_t i=0; i FrameHashCache::GetFramesWithHash(const QByteArray &hash) -{ - QList times; - - for (auto iterator=time_hash_map_.begin();iterator!=time_hash_map_.end();iterator++) { - if (iterator.value() == hash) { - times.append(iterator.key()); - } - } - - return times; -} - -QList FrameHashCache::TakeFramesWithHash(const QByteArray &hash) -{ - TimeRangeList range_to_invalidate; - QList times; - - auto iterator = time_hash_map_.begin(); - - while (iterator != time_hash_map_.end()) { - if (iterator.value() == hash) { - times.append(iterator.key()); - range_to_invalidate.insert(TimeRange(iterator.key(), iterator.key() + timebase_)); - - iterator = time_hash_map_.erase(iterator); - } else { - iterator++; - } - } - - foreach (const TimeRange& r, range_to_invalidate) { - // We apply a 0 job time because the graph hasn't changed to get here, so any renderer should - // be up to date already - Invalidate(r, 0); - } - - return times; -} - -QMap FrameHashCache::time_hash_map() -{ - return time_hash_map_; -} - -QVector FrameHashCache::GetFrameListFromTimeRange(TimeRangeList range_list, const rational &timebase) -{ - // If timebase is null, this will be an infinite loop - Q_ASSERT(!timebase.isNull()); - - QVector times; - - foreach (const TimeRange &range, range_list) { - rational frame = Timecode::snap_time_to_timebase(range.in(), timebase, true); - - while (frame < range.out()) { - times.append(frame); - frame += timebase; - } - } - - return times; -} - -QVector FrameHashCache::GetFrameListFromTimeRange(const TimeRangeList &range) -{ - return GetFrameListFromTimeRange(range, timebase_); -} - -QVector FrameHashCache::GetInvalidatedFrames() -{ - return GetFrameListFromTimeRange(GetInvalidatedRanges()); -} - -QVector FrameHashCache::GetInvalidatedFrames(const TimeRange &intersecting) -{ - return GetFrameListFromTimeRange(GetInvalidatedRanges().Intersects(intersecting)); -} - bool FrameHashCache::SaveCacheFrame(const QByteArray& hash, char* data, const VideoParams& vparam, @@ -313,21 +241,6 @@ FramePtr FrameHashCache::LoadCacheFrame(const QString &fn) return frame; } -void FrameHashCache::LengthChangedEvent(const rational &old, const rational &newlen) -{ - if (newlen < old) { - auto i = time_hash_map_.begin(); - - while (i != time_hash_map_.end()) { - if (i.key() >= newlen) { - i = time_hash_map_.erase(i); - } else { - i++; - } - } - } -} - struct HashTimePair { rational time; QByteArray hash; @@ -335,51 +248,52 @@ struct HashTimePair { void FrameHashCache::ShiftEvent(const rational &from, const rational &to) { - auto i = time_hash_map_.begin(); - // POSITIVE if moving forward -> // NEGATIVE if moving backward <- rational diff = to - from; bool diff_is_negative = (diff < 0); - QList shifted_times; + int64_t to_ts = ToTimestamp(to); + int64_t from_ts = ToTimestamp(from); - while (i != time_hash_map_.end()) { - if (diff_is_negative && i.key() >= to && i.key() < from) { - - // This time will be removed in the shift so we just discard it - i = time_hash_map_.erase(i); - - } else if (i.key() >= from) { - - // This time is after the from time and must be shifted - shifted_times.append({i.key() + diff, i.value()}); - i = time_hash_map_.erase(i); - - } else { - - // Do nothing - i++; - - } + if (from_ts >= GetMapSize()) { + return; } - foreach (const HashTimePair& p, shifted_times) { - time_hash_map_.insert(p.time, p.hash); + if (diff_is_negative) { + // We're moving the frames starting at `from` backwards to where `to` is + if (to_ts < GetMapSize()) { + time_hash_map_.erase(time_hash_map_.begin() + to_ts, time_hash_map_.begin() + from_ts); + } + } else { + // We're moving the frames starting at `from` forwards to where `to` is + if (from_ts < GetMapSize()) { + time_hash_map_.insert(time_hash_map_.begin() + from_ts, to_ts - from_ts, QByteArray()); + } } } void FrameHashCache::InvalidateEvent(const TimeRange &range) { if (!timebase_.isNull()) { - QVector invalid_frames = GetFrameListFromTimeRange({range}); - - foreach (const rational& r, invalid_frames) { - time_hash_map_.remove(r); + int64_t start = ToTimestamp(range.in(), Timecode::kCeil); + int64_t end = ToTimestamp(range.out(), Timecode::kCeil); + for (int64_t i=start; i #include "common/rational.h" +#include "common/timecodefunctions.h" #include "common/timerange.h" #include "codec/frame.h" #include "render/playbackcache.h" @@ -37,24 +38,18 @@ class FrameHashCache : public PlaybackCache public: FrameHashCache(QObject* parent = nullptr); + QByteArray GetHash(const int64_t& time); QByteArray GetHash(const rational& time); + const rational &GetTimebase() const + { + return timebase_; + } + void SetTimebase(const rational& tb); void ValidateFramesWithHash(const QByteArray& hash); - /** - * @brief Returns a list of frames that use a particular hash - */ - QList GetFramesWithHash(const QByteArray& hash); - - /** - * @brief Same as FramesWithHash() but also removes these frames from the map - */ - QList TakeFramesWithHash(const QByteArray& hash); - - QMap time_hash_map(); - /** * @brief Return the path of the cached image at this time */ @@ -70,23 +65,23 @@ public: FramePtr LoadCacheFrame(const QByteArray& hash) const; static FramePtr LoadCacheFrame(const QString& fn); - static QVector GetFrameListFromTimeRange(TimeRangeList range_list, const rational& timebase); - QVector GetFrameListFromTimeRange(const TimeRangeList &range); - QVector GetInvalidatedFrames(); - QVector GetInvalidatedFrames(const TimeRange& intersecting); - -public slots: - void SetHash(const olive::rational& time, const QByteArray& hash, const qint64 &job_time, bool frame_exists); + void SetHash(const olive::rational &time, const QByteArray& hash, bool frame_exists); protected: - virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; - virtual void ShiftEvent(const rational& from, const rational& to) override; virtual void InvalidateEvent(const TimeRange& range) override; private: - QMap time_hash_map_; + rational ToTime(const int64_t &ts) const; + int64_t ToTimestamp(const rational &ts, Timecode::Rounding rounding = Timecode::kRound) const; + + int64_t GetMapSize() const + { + return int64_t(time_hash_map_.size()); + } + + std::vector time_hash_map_; rational timebase_; diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index 2398a3f80..1f50f736d 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -27,64 +27,25 @@ namespace olive { -void PlaybackCache::Invalidate(const TimeRange &r, qint64 job_time) +void PlaybackCache::Invalidate(const TimeRange &r, bool signal) { if (r.in() == r.out()) { qWarning() << "Tried to invalidate zero-length range"; return; } - invalidated_.insert(r); - - RemoveRangeFromJobs(r); - jobs_.append({r, job_time}); + validated_.remove(r); InvalidateEvent(r); - emit Invalidated(r); + if (signal) { + emit Invalidated(r); + } } void PlaybackCache::InvalidateAll() { - if (length_.isNull()) { - return; - } - - Invalidate(TimeRange(0, length_), 0); -} - -void PlaybackCache::SetLength(const rational &r) -{ - if (length_ == r) { - // Same length - do nothing - return; - } - - LengthChangedEvent(length_, r); - - TimeRange range_diff(length_, r); - - if (r.isNull()) { - invalidated_.clear(); - jobs_.clear(); - } else if (r > length_) { - // If new length is greater, simply extend the invalidated range for now - invalidated_.insert(range_diff); - jobs_.append({range_diff, 0}); - } else { - // If new length is smaller, removed hashes - invalidated_.remove(range_diff); - RemoveRangeFromJobs(range_diff); - } - - rational old_length = length_; - length_ = r; - - if (r > old_length) { - emit Invalidated(range_diff); - } else { - emit Validated(range_diff); - } + Invalidate(TimeRange(0, RATIONAL_MAX)); } void PlaybackCache::Shift(rational from, rational to) @@ -93,57 +54,33 @@ void PlaybackCache::Shift(rational from, rational to) return; } - if (from > length_) { - if (to > from) { - // No-op - return; - } else if (to >= length_) { - // No-op - return; - } else { - from = length_; - } - } - - qDebug() << "FIXME: 0 job time may cause cache desyncs"; - // An region between `from` and `to` will be inserted or spliced out - TimeRangeList ranges_to_shift = invalidated_.Intersects(TimeRange(from, RATIONAL_MAX)); + TimeRangeList ranges_to_shift = validated_.Intersects(TimeRange(from, RATIONAL_MAX)); // Remove everything from the minimum point TimeRange remove_range = TimeRange(qMin(from, to), RATIONAL_MAX); - RemoveRangeFromJobs(remove_range); - Validate(remove_range); + Invalidate(remove_range, false); // Shift invalidated ranges // (`diff` is POSITIVE when moving forward -> and NEGATIVE when moving backward <-) rational diff = to - from; foreach (const TimeRange& r, ranges_to_shift) { - Invalidate(r + diff, 0); + Validate(r + diff, false); } ShiftEvent(from, to); - length_ += diff; - - if (diff > 0) { - // If shifting forward, add this section to the invalidated region - Invalidate(TimeRange(from, to), 0); - } - // Emit signals emit Shifted(from, to); } -void PlaybackCache::Validate(const TimeRange &r) +void PlaybackCache::Validate(const TimeRange &r, bool signal) { - invalidated_.remove(r); + validated_.insert(r); - emit Validated(r); -} - -void PlaybackCache::LengthChangedEvent(const rational &, const rational &) -{ + if (signal) { + emit Validated(r); + } } void PlaybackCache::InvalidateEvent(const TimeRange &) @@ -165,29 +102,27 @@ Project *PlaybackCache::GetProject() const return viewer->project(); } -void PlaybackCache::RemoveRangeFromJobs(const TimeRange &remove) +TimeRangeList PlaybackCache::GetInvalidatedRanges(TimeRange intersecting) { - // Code shamelessly copied from TimeRangeList::RemoveTimeRange - for (int i=0;i remove.in()) { - // This element's out point overlaps the range's in, we'll trim it - compare.set_out(remove.in()); - } else if (compare.in() < remove.out() && compare.out() > remove.out()) { - // This element's in point overlaps the range's out, we'll trim it - compare.set_in(remove.out()); - } + // Prevent TimeRange from being below 0, some other behavior in Olive relies on this behavior + // and it seemed reasonable to have safety code in here + intersecting.set_out(qMax(rational(0), intersecting.out())); + intersecting.set_in(qMax(rational(0), intersecting.in())); + + invalidated.insert(intersecting); + + foreach (const TimeRange &range, validated_) { + invalidated.remove(range); } + + return invalidated; +} + +bool PlaybackCache::HasInvalidatedRanges(const TimeRange &intersecting) +{ + return !validated_.contains(intersecting); } QString PlaybackCache::GetCacheDirectory() const @@ -201,4 +136,9 @@ QString PlaybackCache::GetCacheDirectory() const } } +ViewerOutput *PlaybackCache::viewer_parent() const +{ + return dynamic_cast(parent()); +} + } diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index e2fb48fcb..1f66994d2 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -24,51 +24,44 @@ #include #include +#include "common/jobtime.h" #include "common/timerange.h" namespace olive { class Project; +class ViewerOutput; class PlaybackCache : public QObject { Q_OBJECT public: PlaybackCache(QObject* parent = nullptr) : - QObject(parent), - length_(0) + QObject(parent) { } - const rational& GetLength() + TimeRangeList GetInvalidatedRanges(TimeRange intersecting); + TimeRangeList GetInvalidatedRanges(const rational &length) { - return length_; + return GetInvalidatedRanges(TimeRange(0, length)); } - bool IsFullyValidated() + bool HasInvalidatedRanges(const TimeRange &intersecting); + bool HasInvalidatedRanges(const rational &length) { - return invalidated_.isEmpty(); - } - - const TimeRangeList& GetInvalidatedRanges() - { - return invalidated_; - } - - bool HasInvalidatedRanges() - { - return !invalidated_.isEmpty(); + return HasInvalidatedRanges(TimeRange(0, length)); } QString GetCacheDirectory() const; + ViewerOutput *viewer_parent() const; + + void Invalidate(const TimeRange& r, bool signal = true); + public slots: - void Invalidate(const TimeRange& r, qint64 job_time); - void InvalidateAll(); - void SetLength(const rational& r); - void Shift(rational from, rational to); signals: @@ -78,12 +71,8 @@ signals: void Shifted(const olive::rational& from, const olive::rational& to); - void LengthChanged(const olive::rational& r); - protected: - void Validate(const TimeRange& r); - - virtual void LengthChangedEvent(const rational& old, const rational& newlen); + void Validate(const TimeRange& r, bool signal = true); virtual void InvalidateEvent(const TimeRange& range); @@ -91,19 +80,8 @@ protected: Project* GetProject() const; - struct JobIdentifier { - TimeRange range; - qint64 job_time; - }; - - QList jobs_; - private: - void RemoveRangeFromJobs(const TimeRange& remove); - - TimeRangeList invalidated_; - - rational length_; + TimeRangeList validated_; }; diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 916be3318..2b289195a 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -4,6 +4,7 @@ #include #include "codec/conformmanager.h" +#include "core.h" #include "node/project/project.h" #include "render/rendermanager.h" #include "render/renderprocessor.h" @@ -14,10 +15,7 @@ PreviewAutoCacher::PreviewAutoCacher() : viewer_node_(nullptr), has_changed_(false), use_custom_range_(false), - single_frame_render_(nullptr), - last_update_time_(0), - ignore_next_mouse_button_(false), - last_conform_task_(0) + single_frame_render_(nullptr) { paused_ = !Config::Current()[QStringLiteral("AutoCacheEnabled")].toBool(), @@ -63,16 +61,20 @@ void PreviewAutoCacher::SetPaused(bool paused) paused_ = paused; } -void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×, qint64 job_time) +QVector PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×) { - std::vector existing_hashes; + QVector hash_data(times.size()); + + QVector existing_hashes; + + for (int i=0; iGetConnectedTextureOutput(), viewer->GetVideoParams(), time); // Check memory list since disk checking is slow - bool hash_exists = (std::find(existing_hashes.begin(), existing_hashes.end(), hash) != existing_hashes.end()); + bool hash_exists = existing_hashes.contains(hash); if (!hash_exists) { hash_exists = QFileInfo::exists(cache->CachePathName(hash)); @@ -83,40 +85,10 @@ void GenerateHashesInternal(ViewerOutput *viewer, FrameHashCache* cache, const Q } // Set hash in FrameHashCache's thread rather than in ours to prevent race conditions - QMetaObject::invokeMethod(cache, "SetHash", Qt::QueuedConnection, - OLIVE_NS_ARG(rational, time), - Q_ARG(QByteArray, hash), - Q_ARG(qint64, job_time), - Q_ARG(bool, hash_exists)); - } -} - -void PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×, qint64 job_time) -{ - // Ensure number of threads doesn't exceed idealThreadCount for maximum concurrency - int hashes_per_thread = times.size() / qMax(1, QThread::idealThreadCount()-1); - - // Somewhat arbitrary (it felt right) number used to determine when the overhead of sending this - // to threads will exceed the benefit of multithreading - static const int kMinimumHashesPerThread = 500; - if (hashes_per_thread < kMinimumHashesPerThread) { - hashes_per_thread = kMinimumHashesPerThread; + hash_data[i] = {time, hash, hash_exists}; } - // Queue threaded tasks for each - if (hashes_per_thread >= times.size()) { - // Don't bother queuing in other thread, just run - GenerateHashesInternal(viewer, cache, times, job_time); - } else { - QVector > threads; - for (int i=0; imouseButtons() & Qt::LeftButton)) { - ignore_next_mouse_button_ = false; - + if (!Core::instance()->EffectsSliderIsBeingDragged()) { invalidated_video_.insert(range); + video_job_tracker_.insert(range, graph_changed_time_); TryRender(); } @@ -135,7 +106,9 @@ void PreviewAutoCacher::VideoInvalidated(const TimeRange &range) void PreviewAutoCacher::AudioInvalidated(const TimeRange &range) { -// ClearAudioQueue(); + // ClearAudioQueue(); + + audio_job_tracker_.insert(range, graph_changed_time_); // Start jobs to re-render the audio at this range, split into 2 second chunks invalidated_audio_.insert(range); @@ -145,19 +118,33 @@ void PreviewAutoCacher::AudioInvalidated(const TimeRange &range) void PreviewAutoCacher::HashesProcessed() { - QFutureWatcher* watcher = static_cast*>(sender()); + QFutureWatcher< QVector >* watcher = static_cast >*>(sender()); if (hash_tasks_.contains(watcher)) { hash_tasks_.removeOne(watcher); - // Restart delayed requeue timer - delayed_requeue_timer_.stop(); - delayed_requeue_timer_.start(); + // Set all hashes we received + JobTime job_time = watcher->property("job").value(); + auto hashes = watcher->result(); + foreach (auto hash, hashes) { + if (video_job_tracker_.isCurrent(hash.time, job_time)) { + viewer_node_->video_frame_cache()->SetHash(hash.time, hash.hash, hash.exists); + } + } + + if (!hash_iterator_.HasNext()) { + // Restart delayed requeue timer + delayed_requeue_timer_.stop(); + delayed_requeue_timer_.start(); + } } // The cacher might be waiting for this job to finish if (!graph_update_queue_.isEmpty()) { TryRender(); + } else if (hash_iterator_.HasNext()) { + // Launch next hashes + QueueNextHashTask(); } delete watcher; @@ -170,20 +157,23 @@ void PreviewAutoCacher::AudioRendered() if (audio_tasks_.contains(watcher)) { if (watcher->HasResult()) { const TimeRange &range = audio_tasks_.value(watcher); + JobTime watcher_job_time = watcher->property("job").value(); + + TimeRangeList valid_ranges = audio_job_tracker_.getCurrentSubRanges(range, watcher_job_time); AudioVisualWaveform waveform = watcher->GetTicket()->property("waveform").value(); viewer_node_->audio_playback_cache()->WritePCM(range, + valid_ranges, watcher->Get().value(), - &waveform, - watcher->GetTicket()->GetJobTime()); + &waveform); bool pcm_is_usable = true; if (watcher->GetTicket()->property("incomplete").toBool()) { - if (last_conform_task_ > watcher->GetTicket()->GetJobTime()) { + if (last_conform_task_ > watcher_job_time) { // Requeue now - viewer_node_->audio_playback_cache()->Invalidate(range, QDateTime::currentMSecsSinceEpoch()); + viewer_node_->audio_playback_cache()->Invalidate(range); pcm_is_usable = false; } else { // Wait for conform @@ -205,19 +195,15 @@ void PreviewAutoCacher::AudioRendered() } } - if (track) { - QList valid_ranges = viewer_node_->audio_playback_cache()->GetValidRanges(waveform_info.range, - watcher->GetTicket()->GetJobTime()); - if (!valid_ranges.isEmpty()) { - // Generate visual waveform in this background thread - track->waveform().set_channel_count(viewer_node_->GetAudioParams().channel_count()); + if (track && !valid_ranges.isEmpty()) { + // Generate visual waveform in this background thread + track->waveform().set_channel_count(viewer_node_->GetAudioParams().channel_count()); - foreach (const TimeRange& r, valid_ranges) { - track->waveform().OverwriteSums(waveform_info.waveform, r.in(), r.in() - waveform_info.range.in(), r.length()); - } - - emit track->PreviewChanged(); + foreach (const TimeRange& r, valid_ranges) { + track->waveform().OverwriteSums(waveform_info.waveform, r.in(), r.in() - waveform_info.range.in(), r.length()); } + + emit track->PreviewChanged(); } } } @@ -227,7 +213,9 @@ void PreviewAutoCacher::AudioRendered() } // The cacher might be waiting for this job to finish - if (!graph_update_queue_.isEmpty()) { + if (graph_update_queue_.isEmpty()) { + QueueNextAudioTask(); + } else { TryRender(); } @@ -246,6 +234,7 @@ void PreviewAutoCacher::VideoRendered() if (!hash.isEmpty() && VideoParams::FormatIsFloat(viewer_node_->GetVideoParams().format())) { FramePtr frame = watcher->Get().value(); RenderTicketWatcher* w = new RenderTicketWatcher(); + w->setProperty("job", QVariant::fromValue(last_update_time_)); w->setProperty("frame", QVariant::fromValue(frame)); video_download_tasks_.insert(w, hash); connect(w, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoDownloaded); @@ -274,6 +263,8 @@ void PreviewAutoCacher::VideoRendered() TryRender(); } + QueueNextFrameInRange(1); + delete watcher; } @@ -394,9 +385,14 @@ void PreviewAutoCacher::InsertIntoCopyMap(Node *node, Node *copy) Node::CopyInputs(node, copy, false); } +void PreviewAutoCacher::UpdateGraphChangeValue() +{ + graph_changed_time_.Acquire(); +} + void PreviewAutoCacher::UpdateLastSyncedValue() { - last_update_time_ = QDateTime::currentMSecsSinceEpoch(); + last_update_time_.Acquire(); } void PreviewAutoCacher::CancelQueuedSingleFrameRender() @@ -442,11 +438,14 @@ void PreviewAutoCacher::ClearVideoQueue(bool hard) has_changed_ = true; use_custom_range_ = false; + queued_frame_iterator_.reset(); } void PreviewAutoCacher::ClearAudioQueue(bool hard) { ClearQueueInternal(audio_tasks_, hard, &PreviewAutoCacher::AudioRendered); + + audio_iterator_.clear(); } void PreviewAutoCacher::ClearVideoDownloadQueue(bool hard) @@ -457,26 +456,31 @@ void PreviewAutoCacher::ClearVideoDownloadQueue(bool hard) void PreviewAutoCacher::NodeAdded(Node *node) { graph_update_queue_.append({QueuedJob::kNodeAdded, node, NodeInput(), NodeOutput()}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::NodeRemoved(Node *node) { graph_update_queue_.append({QueuedJob::kNodeRemoved, node, NodeInput(), NodeOutput()}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::EdgeAdded(const NodeOutput &output, const NodeInput &input) { graph_update_queue_.append({QueuedJob::kEdgeAdded, nullptr, input, output}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::EdgeRemoved(const NodeOutput &output, const NodeInput &input) { graph_update_queue_.append({QueuedJob::kEdgeRemoved, nullptr, input, output}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::ValueChanged(const NodeInput &input) { graph_update_queue_.append({QueuedJob::kValueChanged, nullptr, input, NodeOutput()}); + UpdateGraphChangeValue(); } void PreviewAutoCacher::TryRender() @@ -493,30 +497,20 @@ void PreviewAutoCacher::TryRender() // If we're here, we must be able to render if (!invalidated_video_.isEmpty()) { - QVector frames = viewer_node_->video_frame_cache()->GetFrameListFromTimeRange(invalidated_video_); + hash_iterator_ = TimeRangeListFrameIterator(invalidated_video_, viewer_node_->video_frame_cache()->GetTimebase()); - QFutureWatcher* watcher = new QFutureWatcher(); - hash_tasks_.append(watcher); - connect(watcher, &QFutureWatcher::finished, this, &PreviewAutoCacher::HashesProcessed); - watcher->setFuture(QtConcurrent::run(&PreviewAutoCacher::GenerateHashes, - copied_viewer_node_, - viewer_node_->video_frame_cache(), - frames, - last_update_time_)); + for (int i=0; i chunks = range.Split(30); + audio_iterator_ = invalidated_audio_; - foreach (const TimeRange& r, chunks) { - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); - audio_tasks_.insert(watcher, r); - watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true)); - } + for (int i=0; isetProperty("hash", hash); + watcher->setProperty("job", QVariant::fromValue(last_update_time_)); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoRendered); video_tasks_.insert(watcher, hash); watcher->SetTicket(RenderManager::instance()->RenderFrame(copied_viewer_node_, @@ -564,7 +559,7 @@ void PreviewAutoCacher::RequeueFrames() delayed_requeue_timer_.stop(); if (viewer_node_ - && viewer_node_->video_frame_cache()->HasInvalidatedRanges() + && viewer_node_->video_frame_cache()->HasInvalidatedRanges(viewer_node_->GetVideoLength()) && hash_tasks_.isEmpty() && has_changed_ && VideoParams::FormatIsFloat(viewer_node_->GetVideoParams().format()) @@ -578,30 +573,10 @@ void PreviewAutoCacher::RequeueFrames() using_range = cache_range_; } - QVector invalidated_ranges = viewer_node_->video_frame_cache()->GetInvalidatedFrames(using_range); + TimeRangeList invalidated = viewer_node_->video_frame_cache()->GetInvalidatedRanges(using_range); + queued_frame_iterator_ = TimeRangeListFrameIterator(invalidated, viewer_node_->video_frame_cache()->GetTimebase()); - foreach (const rational& t, invalidated_ranges) { - const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t); - - RenderTicketWatcher* render_task = video_tasks_.key(hash); - - if (t >= using_range.in() - && t < using_range.out()) { - // We want this hash, if we're not already rendering, start render now - if (!render_task && !video_download_tasks_.key(hash)) { - // Don't render any hash more than once - RenderFrame(hash, t, false, false); - } - } else if (render_task) { - // Cancel this frame unless it's already started - QMutexLocker locker(render_task->GetTicket()->lock()); - - if (!render_task->GetTicket()->IsRunning(false)) { - video_tasks_.remove(render_task); - delete render_task; - } - } - } + QueueNextFrameInRange(RenderManager::GetNumberOfIdealConcurrentJobs()); has_changed_ = false; } @@ -609,21 +584,16 @@ void PreviewAutoCacher::RequeueFrames() void PreviewAutoCacher::ConformFinished() { - last_conform_task_ = QDateTime::currentMSecsSinceEpoch(); + last_conform_task_.Acquire(); if (viewer_node_) { foreach (const TimeRange &range, audio_needing_conform_) { - viewer_node_->audio_playback_cache()->Invalidate(range, QDateTime::currentMSecsSinceEpoch()); + viewer_node_->audio_playback_cache()->Invalidate(range); } audio_needing_conform_.clear(); } } -void PreviewAutoCacher::IgnoreNextMouseButton() -{ - ignore_next_mouse_button_ = true; -} - void PreviewAutoCacher::ForceCacheRange(const TimeRange &range) { has_changed_ = true; @@ -671,6 +641,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) copy_map_.clear(); copied_viewer_node_ = nullptr; graph_update_queue_.clear(); + video_job_tracker_.clear(); + audio_job_tracker_.clear(); // Disconnect signals for future node additions/deletions NodeGraph* graph = viewer_node_->parent(); @@ -709,6 +681,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) // Find copied viewer node copied_viewer_node_ = static_cast(copy_map_.value(viewer_node_)); + copied_viewer_node_->SetViewerVideoCacheEnabled(false); + copied_viewer_node_->SetViewerAudioCacheEnabled(false); copied_color_manager_ = static_cast(copy_map_.value(viewer_node_->project()->color_manager())); // Add all connections @@ -718,6 +692,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) } } + // Ensure graph change value is just before the sync value + UpdateGraphChangeValue(); UpdateLastSyncedValue(); // Connect signals for future node additions/deletions @@ -728,8 +704,10 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) connect(graph, &NodeGraph::ValueChanged, this, &PreviewAutoCacher::ValueChanged); // Copy invalidated ranges - used to determine which frames need hashing - invalidated_video_ = viewer_node_->video_frame_cache()->GetInvalidatedRanges(); - invalidated_audio_ = viewer_node_->audio_playback_cache()->GetInvalidatedRanges(); + invalidated_video_ = viewer_node_->video_frame_cache()->GetInvalidatedRanges(viewer_node_->GetVideoLength()); + video_job_tracker_.insert(invalidated_video_, graph_changed_time_); + invalidated_audio_ = viewer_node_->audio_playback_cache()->GetInvalidatedRanges(viewer_node_->GetAudioLength()); + audio_job_tracker_.insert(invalidated_audio_, graph_changed_time_); connect(viewer_node_->video_frame_cache(), &PlaybackCache::Invalidated, @@ -775,6 +753,70 @@ void PreviewAutoCacher::ClearQueueRemoveEventInternal(QVectorvideo_frame_cache()->GetHash(t); + + RenderTicketWatcher* render_task = video_tasks_.key(hash); + + // We want this hash, if we're not already rendering, start render now + if (!render_task && !video_download_tasks_.key(hash)) { + // Don't render any hash more than once + RenderFrame(hash, t, false, false); + + max--; + } + } +} + +void PreviewAutoCacher::QueueNextHashTask() +{ + // Magic number: dunno what the best number for this is yet + static const int kMaxFrames = 1000; + + QVector times(kMaxFrames); + for (int i=0; i >* watcher = new QFutureWatcher< QVector >(); + watcher->setProperty("job", QVariant::fromValue(last_update_time_)); + hash_tasks_.append(watcher); + connect(watcher, &QFutureWatcher< QVector >::finished, this, &PreviewAutoCacher::HashesProcessed); + watcher->setFuture(QtConcurrent::run(PreviewAutoCacher::GenerateHashes, + copied_viewer_node_, + viewer_node_->video_frame_cache(), + times)); +} + +void PreviewAutoCacher::QueueNextAudioTask() +{ + if (!audio_iterator_.isEmpty()) { + // Copy first range in list + TimeRange r = audio_iterator_.first(); + + // Limit to 30 seconds (FIXME: Hardcoded) + r.set_out(qMin(r.out(), r.in() + 30)); + + // Start job + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + watcher->setProperty("job", QVariant::fromValue(last_update_time_)); + connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); + audio_tasks_.insert(watcher, r); + watcher->SetTicket(RenderManager::instance()->RenderAudio(copied_viewer_node_, r, RenderMode::kOffline, true)); + + audio_iterator_.remove(r); + } +} + template void PreviewAutoCacher::ClearQueueInternal(T& list, bool hard, Func member) { diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 6d0bd4f44..af59b4cf7 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -9,6 +9,7 @@ #include "node/node.h" #include "node/output/viewer/viewer.h" #include "node/project/project.h" +#include "render/renderjobtracker.h" #include "threading/threadticketwatcher.h" namespace olive { @@ -33,16 +34,6 @@ public: */ void SetViewerNode(ViewerOutput *viewer_node); - /** - * @brief If the mouse is held during the next cache invalidation, cache anyway - * - * By default, PreviewAutoCacher ignores invalidations that occur while the mouse is held down, - * assuming that if the mouse is held, the user is dragging something. If you know the mouse will - * be held during a certain action and want PreviewAutoCacher to cache anyway, call this before - * the cache invalidates. - */ - void IgnoreNextMouseButton(); - /** * @brief Returns whether the auto-cache is currently paused or not */ @@ -80,8 +71,6 @@ public: void ClearVideoDownloadQueue(bool wait = false); private: - static void GenerateHashes(ViewerOutput *viewer, FrameHashCache *cache, const QVector& times, qint64 job_time); - void TryRender(); RenderTicketWatcher *RenderFrame(const QByteArray& hash, const rational &time, bool prioritize, bool texture_only); @@ -104,6 +93,7 @@ private: void InsertIntoCopyMap(Node* node, Node* copy); + void UpdateGraphChangeValue(); void UpdateLastSyncedValue(); void CancelQueuedSingleFrameRender(); @@ -115,6 +105,18 @@ private: void ClearQueueRemoveEventInternal(QMap::iterator it); void ClearQueueRemoveEventInternal(QVector::iterator it); + void QueueNextFrameInRange(int max); + void QueueNextHashTask(); + void QueueNextAudioTask(); + + struct HashData { + rational time; + QByteArray hash; + bool exists; + }; + + static QVector GenerateHashes(ViewerOutput *viewer, FrameHashCache* cache, const QVector ×); + class QueuedJob { public: enum Type { @@ -155,21 +157,27 @@ private: RenderTicketPtr single_frame_render_; - QList*> hash_tasks_; + QList >*> hash_tasks_; QMap audio_tasks_; QMap video_tasks_; QMap video_download_tasks_; QMap > video_immediate_passthroughs_; - qint64 last_update_time_; - - bool ignore_next_mouse_button_; + JobTime graph_changed_time_; + JobTime last_update_time_; QTimer delayed_requeue_timer_; TimeRangeList audio_needing_conform_; - qint64 last_conform_task_; + JobTime last_conform_task_; + + RenderJobTracker video_job_tracker_; + RenderJobTracker audio_job_tracker_; + + TimeRangeListFrameIterator queued_frame_iterator_; + TimeRangeListFrameIterator hash_iterator_; + TimeRangeList audio_iterator_; private slots: /** diff --git a/app/render/renderjobtracker.cpp b/app/render/renderjobtracker.cpp new file mode 100644 index 000000000..29f07b3f0 --- /dev/null +++ b/app/render/renderjobtracker.cpp @@ -0,0 +1,71 @@ +/*** + + 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 "renderjobtracker.h" + +namespace olive { + +void RenderJobTracker::insert(const TimeRange &range, JobTime job_time) +{ + // First remove any ranges with this (code copied + TimeRangeList::util_remove(&jobs_, range); + + // Now append the job + TimeRangeWithJob job(range, job_time); + jobs_.append(job); +} + +void RenderJobTracker::insert(const TimeRangeList &ranges, JobTime job_time) +{ + foreach (const TimeRange &r, ranges) { + insert(r, job_time); + } +} + +void RenderJobTracker::clear() +{ + jobs_.clear(); +} + +bool RenderJobTracker::isCurrent(const rational &time, JobTime job_time) const +{ + for (auto it=jobs_.crbegin(); it!=jobs_.crend(); it++) { + if (it->Contains(time)) { + return job_time >= it->GetJobTime(); + } + } + + return false; +} + +TimeRangeList RenderJobTracker::getCurrentSubRanges(const TimeRange &range, const JobTime &job_time) const +{ + TimeRangeList current_ranges; + + for (auto it=jobs_.crbegin(); it!=jobs_.crend(); it++) { + if (job_time >= it->GetJobTime() && it->OverlapsWith(range)) { + current_ranges.insert(it->Intersected(range)); + } + } + + return current_ranges; +} + +} diff --git a/app/render/renderjobtracker.h b/app/render/renderjobtracker.h new file mode 100644 index 000000000..baaddfd72 --- /dev/null +++ b/app/render/renderjobtracker.h @@ -0,0 +1,68 @@ +/*** + + 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 RENDERJOBTRACKER_H +#define RENDERJOBTRACKER_H + +#include "common/jobtime.h" +#include "common/timerange.h" + +namespace olive { + +class RenderJobTracker +{ +public: + RenderJobTracker() = default; + + void insert(const TimeRange &range, JobTime job_time); + void insert(const TimeRangeList &ranges, JobTime job_time); + + void clear(); + + bool isCurrent(const rational &time, JobTime job_time) const; + + TimeRangeList getCurrentSubRanges(const TimeRange &range, const JobTime &job_time) const; + +private: + class TimeRangeWithJob : public TimeRange + { + public: + TimeRangeWithJob() = default; + TimeRangeWithJob(const TimeRange &range, const JobTime &job_time) + { + set_range(range.in(), range.out()); + job_time_ = job_time; + } + + JobTime GetJobTime() const {return job_time_;} + void SetJobTime(JobTime jt) {job_time_ = jt;} + + private: + JobTime job_time_; + + }; + + QVector jobs_; + +}; + +} + +#endif // RENDERJOBTRACKER_H diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h index 34f8532d9..816394dac 100644 --- a/app/render/rendermanager.h +++ b/app/render/rendermanager.h @@ -123,6 +123,11 @@ public: return backend_; } + static int GetNumberOfIdealConcurrentJobs() + { + return QThread::idealThreadCount(); + } + signals: private: diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 814c31125..425d51f7b 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -147,9 +147,8 @@ bool ExportTask::Run() return success; } -void ExportTask::FrameDownloaded(FramePtr f, const QByteArray &hash, const QVector ×, qint64 job_time) +void ExportTask::FrameDownloaded(FramePtr f, const QByteArray &hash, const QVector ×) { - Q_UNUSED(job_time) Q_UNUSED(hash) foreach (const rational& t, times) { @@ -179,10 +178,8 @@ void ExportTask::FrameDownloaded(FramePtr f, const QByteArray &hash, const QVect } } -void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples, qint64 job_time) +void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples) { - Q_UNUSED(job_time) - TimeRange adjusted_range = range; if (params_.has_custom_range()) { diff --git a/app/task/export/export.h b/app/task/export/export.h index 7f1c0859a..f05440a8f 100644 --- a/app/task/export/export.h +++ b/app/task/export/export.h @@ -38,9 +38,9 @@ public: protected: virtual bool Run() override; - virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times, qint64 job_time) override; + virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times) override; - virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; + virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) override; virtual void EncodeSubtitle(const SubtitleBlock *sub) override; diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp index 1f6adc80e..2f207cf9c 100644 --- a/app/task/precache/precachetask.cpp +++ b/app/task/precache/precachetask.cpp @@ -57,13 +57,18 @@ PreCacheTask::~PreCacheTask() bool PreCacheTask::Run() { // Get list of invalidated ranges - TimeRangeList video_range = viewer()->video_frame_cache()->GetInvalidatedRanges(); + TimeRange intersection; - // If we're caching only in-out, limit the range to that if (footage_->GetTimelinePoints()->workarea()->enabled()) { - video_range = video_range.Intersects(footage_->GetTimelinePoints()->workarea()->range()); + // If we're caching only in-out, limit the range to that + intersection = footage_->GetTimelinePoints()->workarea()->range(); + } else { + // Otherwise use full length + intersection = TimeRange(0, footage_->GetVideoLength()); } + TimeRangeList video_range = viewer()->video_frame_cache()->GetInvalidatedRanges(intersection); + Render(project_->color_manager(), video_range, TimeRangeList(), @@ -74,7 +79,7 @@ bool PreCacheTask::Run() return true; } -void PreCacheTask::FrameDownloaded(FramePtr frame, const QByteArray &hash, const QVector ×, qint64 job_time) +void PreCacheTask::FrameDownloaded(FramePtr frame, const QByteArray &hash, const QVector ×) { // Do nothing. Pre-cache essentially just creates more frames in the cache, it doesn't need to do // anything else. @@ -82,16 +87,14 @@ void PreCacheTask::FrameDownloaded(FramePtr frame, const QByteArray &hash, const Q_UNUSED(frame) Q_UNUSED(hash) Q_UNUSED(times) - Q_UNUSED(job_time) } -void PreCacheTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples, qint64 job_time) +void PreCacheTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples) { // Pre-cache doesn't cache any audio Q_UNUSED(range) Q_UNUSED(samples) - Q_UNUSED(job_time) } } diff --git a/app/task/precache/precachetask.h b/app/task/precache/precachetask.h index 325a49b47..a9bf6497d 100644 --- a/app/task/precache/precachetask.h +++ b/app/task/precache/precachetask.h @@ -38,9 +38,9 @@ public: protected: virtual bool Run() override; - virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times, qint64 job_time) override; + virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times) override; - virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; + virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) override; private: Project* project_; diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 229141921..e747aec33 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -58,7 +58,7 @@ bool ProjectLoadTask::Run() // Project is newer than we support SetError(tr("This project is newer than this version of Olive and cannot be opened.")); return false; - } else if (project_version < 210122) { // Change this if we drop support for a project version + } else if (project_version < 210528) { // Change this if we drop support for a project version // Project is older than we support SetError(tr("This project is from a version of Olive that is no longer supported in this version.")); return false; diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp index 0d817104a..1d672ace4 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 { @@ -86,7 +87,7 @@ bool LoadOTIOTask::Run() Sequence* sequence = new Sequence(); sequence->SetLabel(QString::fromStdString(timeline->name())); sequence->setParent(project_); - FolderAddChild(project_->root(), sequence).redo(); + FolderAddChild(project_->root(), sequence).redo_now(); // FIXME: As far as I know, OTIO doesn't store video/audio parameters? sequence->set_default_parameters(); @@ -110,7 +111,7 @@ bool LoadOTIOTask::Run() // Create track TimelineAddTrackCommand t(sequence->track_list(type)); - t.redo(); + t.redo_now(); track = t.track(); } else { qWarning() << "Found unknown track type:" << otio_track->kind().c_str(); diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index ee559fe2c..2f78a9ff0 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -55,8 +55,6 @@ bool RenderTask::Render(ColorManager* manager, double total_length = 0; // Store real time before any rendering takes place - qint64 job_time = QDateTime::currentMSecsSinceEpoch(); - // Queue audio jobs foreach (const TimeRange& range, audio_range) { // Don't count audio progress, since it's generally a lot faster than video and is weighted at @@ -84,16 +82,19 @@ bool RenderTask::Render(ColorManager* manager, if (!video_range.isEmpty()) { // Get list of discrete frames from range - QVector times = FrameHashCache::GetFrameListFromTimeRange(video_range, video_params().frame_rate_as_time_base()); - QVector hashes(times.size()); + TimeRangeListFrameIterator iterator(video_range, video_params().frame_rate_as_time_base()); + QVector times(iterator.size()); + QVector hashes(iterator.size()); // Generate hashes - for (int i=0; iHash(viewer()->GetConnectedTextureOutput(), video_params_, times.at(i)); + times[i] = r; + hashes[i] = RenderManager::instance()->Hash(viewer()->GetConnectedTextureOutput(), video_params_, r); } // Filter out duplicates @@ -190,9 +191,7 @@ bool RenderTask::Render(ColorManager* manager, TimeRange range = watcher->property("range").value(); - AudioDownloaded(range, - watcher->Get().value(), - job_time); + AudioDownloaded(range, watcher->Get().value()); // Don't count audio progress, since it's generally a lot faster than video and is weighted at // 50%, which makes the progress bar look weird to the uninitiated @@ -214,7 +213,7 @@ bool RenderTask::Render(ColorManager* manager, // Assume single-step video or video download ticket QByteArray rendered_hash = watcher->property("hash").toByteArray(); - FrameDownloaded(watcher->Get().value(), rendered_hash, time_map.value(rendered_hash), job_time); + FrameDownloaded(watcher->Get().value(), rendered_hash, time_map.value(rendered_hash)); if (native_progress_signalling_) { double progress_to_add = 1.0; diff --git a/app/task/render/render.h b/app/task/render/render.h index af11071a7..a41d9a55e 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -51,9 +51,9 @@ protected: virtual void DownloadFrame(QThread* thread, FramePtr frame, const QByteArray &hash); - virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times, qint64 job_time) = 0; + virtual void FrameDownloaded(FramePtr frame, const QByteArray& hash, const QVector& times) = 0; - virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) = 0; + virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) = 0; virtual void EncodeSubtitle(const SubtitleBlock *subtitle); diff --git a/app/threading/threadticket.cpp b/app/threading/threadticket.cpp index 5dfd29d4c..d8d041d9e 100644 --- a/app/threading/threadticket.cpp +++ b/app/threading/threadticket.cpp @@ -27,7 +27,6 @@ RenderTicket::RenderTicket() : has_result_(false), finish_count_(0) { - SetJobTime(); } void RenderTicket::WaitForFinished(QMutex *mutex) diff --git a/app/threading/threadticket.h b/app/threading/threadticket.h index 47f2bb586..d4035c832 100644 --- a/app/threading/threadticket.h +++ b/app/threading/threadticket.h @@ -38,16 +38,6 @@ class RenderTicket : public QObject public: RenderTicket(); - qint64 GetJobTime() const - { - return job_time_; - } - - void SetJobTime() - { - job_time_ = QDateTime::currentMSecsSinceEpoch(); - } - /** * @brief Get the ticket's current state * @@ -137,8 +127,6 @@ private: QWaitCondition wait_; - qint64 job_time_; - }; using RenderTicketPtr = std::shared_ptr; diff --git a/app/undo/undocommand.cpp b/app/undo/undocommand.cpp index d00a327ec..075348c15 100644 --- a/app/undo/undocommand.cpp +++ b/app/undo/undocommand.cpp @@ -24,23 +24,39 @@ namespace olive { +MultiUndoCommand::MultiUndoCommand() : + done_(false) +{ +} + void MultiUndoCommand::redo() { - for (auto it=children_.cbegin(); it!=children_.cend(); it++) { - (*it)->redo_and_set_modified(); + if (!done_) { + for (auto it=children_.cbegin(); it!=children_.cend(); it++) { + (*it)->redo_and_set_modified(); + } + done_ = true; } } void MultiUndoCommand::undo() { - for (auto it=children_.crbegin(); it!=children_.crend(); it++) { - (*it)->undo_and_set_modified(); + if (done_) { + for (auto it=children_.crbegin(); it!=children_.crend(); it++) { + (*it)->undo_and_set_modified(); + } + done_ = false; } } +UndoCommand::UndoCommand() +{ + prepared_ = false; +} + void UndoCommand::redo_and_set_modified() { - redo(); + redo_now(); project_ = GetRelevantProject(); if (project_) { @@ -51,11 +67,26 @@ void UndoCommand::redo_and_set_modified() void UndoCommand::undo_and_set_modified() { - undo(); + undo_now(); if (project_) { project_->set_modified(modified_); } } +void UndoCommand::redo_now() +{ + if (!prepared_) { + prepare(); + prepared_ = true; + } + + redo(); +} + +void UndoCommand::undo_now() +{ + undo(); +} + } diff --git a/app/undo/undocommand.h b/app/undo/undocommand.h index 54f663d64..f63ae745e 100644 --- a/app/undo/undocommand.h +++ b/app/undo/undocommand.h @@ -34,17 +34,19 @@ class Project; class UndoCommand { public: - UndoCommand() = default; + UndoCommand(); virtual ~UndoCommand(){} DISABLE_COPY_MOVE(UndoCommand) - virtual void redo() = 0; - virtual void undo() = 0; + bool has_prepared() const {return prepared_;} + void set_prepared(bool e) {prepared_ = true;} + + void redo_now(); + void undo_now(); void redo_and_set_modified(); - void undo_and_set_modified(); virtual Project* GetRelevantProject() const = 0; @@ -59,6 +61,11 @@ public: name_ = name; } +protected: + virtual void prepare(){} + virtual void redo() = 0; + virtual void undo() = 0; + private: bool modified_; @@ -66,12 +73,14 @@ private: Project* project_; + bool prepared_; + }; class MultiUndoCommand : public UndoCommand { public: - MultiUndoCommand() = default; + MultiUndoCommand(); virtual void redo() override; virtual void undo() override; @@ -99,6 +108,8 @@ public: private: std::vector children_; + bool done_; + }; } diff --git a/app/widget/keyframeview/keyframeviewundo.h b/app/widget/keyframeview/keyframeviewundo.h index e8fc3aa55..c994d6f1a 100644 --- a/app/widget/keyframeview/keyframeviewundo.h +++ b/app/widget/keyframeview/keyframeviewundo.h @@ -32,6 +32,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -51,6 +52,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 605964f84..b85fbc69d 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -30,8 +30,10 @@ namespace olive { +#define super TimeBasedWidget + NodeParamView::NodeParamView(QWidget *parent) : - TimeBasedWidget(true, false, parent), + super(true, false, parent), last_scroll_val_(0), focused_node_(nullptr) { @@ -194,7 +196,7 @@ void NodeParamView::DeselectNodes(const QVector &nodes) void NodeParamView::resizeEvent(QResizeEvent *event) { - QWidget::resizeEvent(event); + super::resizeEvent(event); vertical_scrollbar_->setPageStep(vertical_scrollbar_->height()); @@ -203,14 +205,14 @@ void NodeParamView::resizeEvent(QResizeEvent *event) void NodeParamView::ScaleChangedEvent(const double &scale) { - TimeBasedWidget::ScaleChangedEvent(scale); + super::ScaleChangedEvent(scale); keyframe_view_->SetScale(scale); } void NodeParamView::TimebaseChangedEvent(const rational &timebase) { - TimeBasedWidget::TimebaseChangedEvent(timebase); + super::TimebaseChangedEvent(timebase); keyframe_view_->SetTimebase(timebase); @@ -223,7 +225,7 @@ void NodeParamView::TimebaseChangedEvent(const rational &timebase) void NodeParamView::TimeChangedEvent(const int64_t ×tamp) { - TimeBasedWidget::TimeChangedEvent(timestamp); + super::TimeChangedEvent(timestamp); keyframe_view_->SetTime(timestamp); diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 0c4f05a70..ad01f6ba0 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -62,13 +62,15 @@ NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : this->setWidget(body_); + // Use dummy QWidget to retain width when not expanded (QDockWidget seems to ignore the titlebar + // size hints and will shrink as small as possible if the body is hidden) + hidden_body_ = new QWidget(this); + connect(node_, &Node::LabelChanged, this, &NodeParamViewItem::Retranslate); setBackgroundRole(QPalette::Base); setAutoFillBackground(true); - setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - setFocusPolicy(Qt::ClickFocus); Retranslate(); @@ -88,7 +90,7 @@ void NodeParamViewItem::SetTime(const rational &time) void NodeParamViewItem::SetTimebase(const rational& timebase) { - body_->SetTimebase(timebase); + body_->SetTimebase(timebase); } Node *NodeParamViewItem::GetNode() const @@ -140,7 +142,7 @@ void NodeParamViewItem::Retranslate() void NodeParamViewItem::SetExpanded(bool e) { - body_->setVisible(e); + setWidget(e ? body_ : hidden_body_); title_bar_->SetExpanded(e); emit ExpandedChanged(e); @@ -537,10 +539,10 @@ void NodeParamViewItemBody::ToggleArrayExpanded() } } -void NodeParamViewItemBody::SetTimebase(const rational& timebase) +void NodeParamViewItemBody::SetTimebase(const rational& timebase) { foreach (const InputUI& ui_obj, input_ui_map_) { - ui_obj.widget_bridge->SetTimebase(timebase); + ui_obj.widget_bridge->SetTimebase(timebase); } } diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index 30ae562e9..b000b6282 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -216,6 +216,8 @@ private: NodeParamViewItemBody* body_; + QWidget *hidden_body_; + Node* node_; rational time_; diff --git a/app/widget/nodeparamview/nodeparamviewundo.h b/app/widget/nodeparamview/nodeparamviewundo.h index ffc2f5b99..2e9623652 100644 --- a/app/widget/nodeparamview/nodeparamviewundo.h +++ b/app/widget/nodeparamview/nodeparamviewundo.h @@ -35,6 +35,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -51,6 +52,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -70,6 +72,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -90,6 +93,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -109,6 +113,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -128,6 +133,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -146,6 +152,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 8dfe37d58..78d54cc26 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -62,6 +62,11 @@ void NodeParamViewWidgetBridge::SetTime(const rational &time) } } +int GetSliderCount(NodeValue::Type type) +{ + return NodeValue::get_number_of_keyframe_tracks(type); +} + void NodeParamViewWidgetBridge::CreateWidgets() { if (input_.IsArray() && input_.element() == -1) { @@ -73,7 +78,8 @@ void NodeParamViewWidgetBridge::CreateWidgets() } else { // We assume the first data type is the "primary" type - switch (input_.GetDataType()) { + NodeValue::Type t = input_.GetDataType(); + switch (t) { // None of these inputs have applicable UI widgets case NodeValue::kNone: case NodeValue::kTexture: @@ -89,29 +95,17 @@ void NodeParamViewWidgetBridge::CreateWidgets() CreateSliders(1); break; } - case NodeValue::kFloat: - { - CreateSliders(1); - break; - } case NodeValue::kRational: { CreateSliders(1); break; } + case NodeValue::kFloat: case NodeValue::kVec2: - { - CreateSliders(2); - break; - } case NodeValue::kVec3: - { - CreateSliders(3); - break; - } case NodeValue::kVec4: { - CreateSliders(4); + CreateSliders(GetSliderCount(t)); break; } case NodeValue::kCombo: @@ -410,6 +404,7 @@ void NodeParamViewWidgetBridge::CreateSliders(int count) T* fs = new T(); fs->SliderBase::SetDefaultValue(input_.GetSplitDefaultValueForTrack(i)); fs->SetLadderElementCount(2); + fs->SetIsEffectsSlider(true); widgets_.append(fs); connect(fs, &T::ValueChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); } diff --git a/app/widget/nodeview/CMakeLists.txt b/app/widget/nodeview/CMakeLists.txt index 00e78e7db..e73798ae5 100644 --- a/app/widget/nodeview/CMakeLists.txt +++ b/app/widget/nodeview/CMakeLists.txt @@ -23,8 +23,12 @@ set(OLIVE_SOURCES widget/nodeview/nodeviewedge.h widget/nodeview/nodeviewitem.cpp widget/nodeview/nodeviewitem.h + widget/nodeview/nodeviewminimap.cpp + widget/nodeview/nodeviewminimap.h widget/nodeview/nodeviewscene.cpp widget/nodeview/nodeviewscene.h + widget/nodeview/nodeviewtoolbar.cpp + widget/nodeview/nodeviewtoolbar.h widget/nodeview/nodeviewundo.cpp widget/nodeview/nodeviewundo.h PARENT_SCOPE diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 118583f0f..6f277e5a9 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -20,12 +20,15 @@ #include "nodeview.h" +#include #include #include #include #include "core.h" #include "nodeviewundo.h" +#include "node/audio/volume/volume.h" +#include "node/distort/transform/transformdistortnode.h" #include "node/factory.h" #include "node/traverser.h" #include "widget/menu/menushared.h" @@ -44,8 +47,10 @@ NodeView::NodeView(QWidget *parent) : create_edge_(nullptr), create_edge_dst_(nullptr), create_edge_dst_temp_expanded_(false), - filter_mode_(kFilterShowSelectedBlocks), - scale_(1.0) + paste_command_(nullptr), + filter_mode_(kFilterShowSelective), + scale_(1.0), + queue_reposition_contexts_(false) { setScene(&scene_); SetDefaultDragMode(RubberBandDrag); @@ -60,58 +65,94 @@ NodeView::NodeView(QWidget *parent) : SetFlowDirection(NodeViewCommon::kTopToBottom); - // Set massive scene rect and hide the scrollbars to create an "infinite space" effect - scene_.setSceneRect(-1000000, -1000000, 2000000, 2000000); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + UpdateSceneBoundingRect(); + connect(&scene_, &QGraphicsScene::changed, this, &NodeView::UpdateSceneBoundingRect); + + minimap_ = new NodeViewMiniMap(&scene_, this); + minimap_->show(); + connect(minimap_, &NodeViewMiniMap::Resized, this, &NodeView::RepositionMiniMap); + connect(minimap_, &NodeViewMiniMap::MoveToScenePoint, this, &NodeView::MoveToScenePoint); + connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap); + + viewport()->installEventFilter(this); } NodeView::~NodeView() { // Unset the current graph - SetGraph(nullptr); + ClearGraph(); } -void NodeView::SetGraph(NodeGraph *graph) +void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) { - if (graph_ == graph) { - return; - } + bool graph_changed = graph_ != graph; + bool context_changed = last_set_filter_nodes_ != nodes; - if (graph_) { - disconnect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); - disconnect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); - disconnect(graph_, &NodeGraph::InputConnected, &scene_, &NodeViewScene::AddEdge); - disconnect(graph_, &NodeGraph::InputDisconnected, &scene_, &NodeViewScene::RemoveEdge); + if (graph_changed || context_changed) { + // Clear nodes if necessary + bool refresh_required = (graph_changed && filter_mode_ == kFilterShowAll) + || (context_changed && filter_mode_ == kFilterShowSelective); + bool nodes_visible = (graph && filter_mode_ == kFilterShowAll) + || (!nodes.isEmpty() && filter_mode_ == kFilterShowSelective); - DeselectAll(); - - // Clear the scene of all UI objects - scene_.clear(); - } - - // Set reference to the graph - graph_ = graph; - - // If the graph is valid, add UI objects for each of its Nodes - if (graph_) { - connect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); - connect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); - connect(graph_, &NodeGraph::InputConnected, &scene_, &NodeViewScene::AddEdge); - connect(graph_, &NodeGraph::InputDisconnected, &scene_, &NodeViewScene::RemoveEdge); - - foreach (Node* n, graph_->nodes()) { - scene_.AddNode(n); + if (refresh_required) { + DeselectAll(); + positions_.clear(); + scene_.clear(); + context_offsets_.clear(); } - foreach (Node* n, graph_->nodes()) { - for (auto it=n->input_connections().cbegin(); it!=n->input_connections().cend(); it++) { - scene_.AddEdge(it->second, it->first); + // Handle graph change + if (graph_changed) { + if (graph_) { + // Disconnect from current graph + disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); + disconnect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); + disconnect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); + disconnect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); + disconnect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); + } + + graph_ = graph; + + if (graph_) { + // Connect to new graph + connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); + connect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); + connect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); + connect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); + connect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); } } + + if (context_changed) { + last_set_filter_nodes_ = nodes; + + if (filter_mode_ == kFilterShowSelective) { + filter_nodes_ = nodes; + } + } + + if (refresh_required && nodes_visible) { + if (filter_mode_ == kFilterShowAll) { + // Just make the filter nodes all of the graph's contexts + filter_nodes_ = graph->GetPositionMap().keys().toVector(); + } + + RepositionContexts(); + + // Center on something + QMetaObject::invokeMethod(this, &NodeView::CenterOnItemsBoundingRect, Qt::QueuedConnection); + } } } +void NodeView::ClearGraph() +{ + SetGraph(nullptr, QVector()); +} + void NodeView::DeleteSelected() { if (!graph_) { @@ -121,14 +162,25 @@ void NodeView::DeleteSelected() MultiUndoCommand* command = new MultiUndoCommand(); { + // First remove any selected edges QVector selected_edges = scene_.GetSelectedEdges(); - foreach (NodeViewEdge* edge, selected_edges) { - command->add_child(new NodeEdgeRemoveCommand(edge->output(), edge->input())); + if (!selected_edges.isEmpty()) { + Node::OutputConnections removed_connections(selected_edges.size()); + + for (int i=0; iadd_child(new NodeEdgeRemoveCommand(edge->output(), edge->input())); + removed_connections[i] = {edge->output(), edge->input()}; + } + + // Update contexts + UpdateContextsFromEdgeRemove(command, removed_connections); } } { + // Secondly remove any nodes QVector selected_nodes = scene_.GetSelectedNodes(); // Ensure no nodes are "undeletable" @@ -140,7 +192,7 @@ void NodeView::DeleteSelected() } if (!selected_nodes.isEmpty()) { - foreach (Node* node, selected_nodes) { + for (Node* node : qAsConst(selected_nodes)) { command->add_child(new NodeRemoveAndDisconnectCommand(node)); } } @@ -169,7 +221,7 @@ void NodeView::SelectAll() } else { // We have to determine the difference QVector new_selection; - foreach (Node* n, graph_->nodes()) { + for (Node* n : graph_->nodes()) { if (!selected_nodes_.contains(n)) { new_selection.append(n); } @@ -201,7 +253,7 @@ void NodeView::DeselectAll() selected_nodes_.clear(); } -void NodeView::Select(QVector nodes) +void NodeView::Select(QVector nodes, bool center_view_on_item) { if (!graph_) { return; @@ -219,7 +271,9 @@ void NodeView::Select(QVector nodes) // Remove any duplicates QVector processed; - foreach (Node* n, nodes) { + NodeViewItem *first_item = nullptr; + + for (Node* n : qAsConst(nodes)) { if (processed.contains(n)) { continue; } @@ -228,17 +282,25 @@ void NodeView::Select(QVector nodes) NodeViewItem* item = scene_.NodeToUIObject(n); - item->setSelected(true); + if (item) { + item->setSelected(true); - if (deselections.contains(n)) { - deselections.removeOne(n); - } else { - new_selections.append(n); + if (!first_item) { + first_item = item; + } + + if (deselections.contains(n)) { + deselections.removeOne(n); + } else { + new_selections.append(n); + } } } // Center on something - centerOn(scene_.NodeToUIObject(nodes.first())); + if (center_view_on_item && first_item) { + centerOn(first_item); + } ConnectSelectionChangedSignal(); @@ -256,7 +318,7 @@ void NodeView::Select(QVector nodes) selected_nodes_ = nodes; } -void NodeView::SelectWithDependencies(QVector nodes) +void NodeView::SelectWithDependencies(QVector nodes, bool center_view_on_item) { if (!graph_) { return; @@ -267,7 +329,7 @@ void NodeView::SelectWithDependencies(QVector nodes) nodes.append(nodes.at(i)->GetDependencies()); } - Select(nodes); + Select(nodes, center_view_on_item); } void NodeView::CopySelected(bool cut) @@ -295,15 +357,15 @@ void NodeView::Paste() return; } - MultiUndoCommand* command = new MultiUndoCommand(); + paste_command_ = new MultiUndoCommand(); - QVector pasted_nodes = PasteNodesFromClipboard(graph_, command); + QVector pasted_nodes = PasteNodesFromClipboard(graph_, paste_command_); if (!pasted_nodes.isEmpty()) { - command->add_child(new NodeViewAttachNodesToCursor(this, pasted_nodes)); + paste_command_->add_child(new NodeViewAttachNodesToCursor(this, pasted_nodes)); } - Core::instance()->undo_stack()->pushIfHasChildren(command); + paste_command_->redo(); } void NodeView::Duplicate() @@ -318,20 +380,20 @@ void NodeView::Duplicate() return; } - MultiUndoCommand* command = new MultiUndoCommand(); + paste_command_ = new MultiUndoCommand(); - QVector duplicated_nodes = Node::CopyDependencyGraph(selected, command); + QVector duplicated_nodes = Node::CopyDependencyGraph(selected, paste_command_); if (!duplicated_nodes.isEmpty()) { - command->add_child(new NodeViewAttachNodesToCursor(this, duplicated_nodes)); + paste_command_->add_child(new NodeViewAttachNodesToCursor(this, duplicated_nodes)); } - Core::instance()->undo_stack()->pushIfHasChildren(command); + paste_command_->redo(); } void NodeView::SetColorLabel(int index) { - foreach (Node* node, selected_nodes_) { + for (Node* node : qAsConst(selected_nodes_)) { node->SetOverrideColor(index); } } @@ -348,14 +410,69 @@ void NodeView::ZoomOut() void NodeView::keyPressEvent(QKeyEvent *event) { - super::keyPressEvent(event); + switch (event->key()) { + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Up: + case Qt::Key_Down: + { + if (graph_) { + MultiUndoCommand *pos_command = new MultiUndoCommand(); + for (Node *n : qAsConst(selected_nodes_)) { + for (Node *context : qAsConst(filter_nodes_)) { + if (graph_->GetNodesForContext(context).contains(n)) { + QPointF old_pos = graph_->GetNodePosition(n, context); - if (event->key() == Qt::Key_Escape && !attached_items_.isEmpty()) { - DetachItemsFromCursor(); + // Determine one pixel in scene units + double movement_amt = 1.0 / scale_; - // We undo the last action which SHOULD be adding the node - // FIXME: Possible danger of this not being the case? - Core::instance()->undo_stack()->undo(); + // Translate to 2D movement + QPointF node_movement; + switch (event->key()) { + case Qt::Key_Left: + node_movement.setX(-movement_amt); + break; + case Qt::Key_Right: + node_movement.setX(movement_amt); + break; + case Qt::Key_Up: + node_movement.setY(-movement_amt); + break; + case Qt::Key_Down: + node_movement.setY(movement_amt); + break; + } + + // Translate from screen units into node units + node_movement = NodeViewItem::ScreenToNodePoint(node_movement, scene_.GetFlowDirection()); + + // Move command + pos_command->add_child(new NodeSetPositionCommand(n, context, old_pos + node_movement, false)); + } + } + } + Core::instance()->undo_stack()->pushIfHasChildren(pos_command); + } + break; + } + case Qt::Key_Escape: + if (!attached_items_.isEmpty()) { + DetachItemsFromCursor(); + + // We undo the last action which SHOULD be adding the node + if (paste_command_) { + paste_command_->undo(); + delete paste_command_; + paste_command_ = nullptr; + } + + break; + } + + /* fall through */ + default: + super::keyPressEvent(event); + break; } } @@ -363,19 +480,31 @@ void NodeView::mousePressEvent(QMouseEvent *event) { if (HandPress(event)) return; - QGraphicsItem* item = itemAt(event->pos()); - if (event->button() == Qt::LeftButton) { - NodeViewEdge* edge_item = dynamic_cast(item); - if (edge_item && edge_item->arrow_bounding_rect().contains(mapToScene(event->pos()))) { - create_edge_src_ = scene_.NodeToUIObject(edge_item->output().node()); - create_edge_src_output_ = edge_item->output().output(); - create_edge_ = edge_item; - create_edge_already_exists_ = true; - return; + // See if we're dragging the arrow of an edge + QPointF scene_pt = mapToScene(event->pos()); + + for (NodeViewEdge *edge_item : scene_.edges()) { + if (edge_item->arrow_bounding_rect().contains(scene_pt)) { + create_edge_src_ = scene_.NodeToUIObject(edge_item->output().node()); + create_edge_src_output_ = edge_item->output().output(); + create_edge_ = edge_item; + create_edge_already_exists_ = true; + return; + } + } + + // See if we're dragging the arrow of a node + for (NodeViewItem *node_item : scene_.item_map()) { + if (node_item->GetOutputTriangle().boundingRect().translated(node_item->pos()).contains(scene_pt)) { + CreateNewEdge(node_item); + return; + } } } + QGraphicsItem* item = itemAt(event->pos()); + if (event->button() == Qt::RightButton) { if (!item || !item->isSelected()) { // Qt doesn't do this by default for some reason @@ -393,15 +522,7 @@ void NodeView::mousePressEvent(QMouseEvent *event) if (event->modifiers() & Qt::ControlModifier) { NodeViewItem* node_item = dynamic_cast(item); if (node_item) { - create_edge_ = new NodeViewEdge(); - create_edge_src_ = node_item; - create_edge_src_output_ = Node::kDefaultOutput; - create_edge_already_exists_ = false; - - create_edge_->SetCurved(scene_.GetEdgesAreCurved()); - create_edge_->SetFlowDirection(scene_.GetFlowDirection()); - - scene_.addItem(create_edge_); + CreateNewEdge(node_item); return; } } @@ -500,7 +621,7 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) NodeViewEdge* new_drop_edge = nullptr; // See if there is an edge here - foreach (QGraphicsItem* item, items) { + for (QGraphicsItem* item : qAsConst(items)) { new_drop_edge = dynamic_cast(item); if (new_drop_edge) { @@ -517,7 +638,7 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) // Iterate through the inputs of our dragging node and see if our node has any acceptable // inputs to connect to for this type - foreach (const QString& input, attached_node->inputs()) { + for (const QString& input : attached_node->inputs()) { NodeInput i(attached_node, input); if (attached_node->IsInputConnectable(input)) { @@ -560,13 +681,26 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) if (HandRelease(event)) return; if (create_edge_) { + // We are creating a new edge or moving an existing one MultiUndoCommand* command = new MultiUndoCommand(); + Node::OutputConnections removed_edges; + Node::OutputConnection added_edge; + + bool reconnected_to_itself = false; + if (create_edge_already_exists_) { - if (!create_edge_->IsConnected()) { + if (create_edge_dst_input_ == create_edge_->input()) { + reconnected_to_itself = true; + } else { + // We are moving (or removing) an existing edge command->add_child(new NodeEdgeRemoveCommand(create_edge_->output(), create_edge_->input())); + + // Update contexts for edge removal + removed_edges.push_back({create_edge_->output(), create_edge_->input()}); } } else { + // We're creating a new edge, which means this UI object is only temporary delete create_edge_; } @@ -582,46 +716,164 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) create_edge_dst_->setZValue(0); } - if (create_edge_dst_input_.IsValid()) { + NodeInput &creating_input = create_edge_dst_input_; + if (creating_input.IsValid()) { // Make connection - command->add_child(new NodeEdgeAddCommand(NodeOutput(create_edge_src_->GetNode(), create_edge_src_output_), create_edge_dst_input_)); - create_edge_dst_input_.Reset(); + if (!reconnected_to_itself) { + NodeOutput creating_output(create_edge_src_->GetNode(), create_edge_src_output_); + + if (creating_input.IsConnected()) { + Node::OutputConnection existing_edge_to_remove = {creating_input.GetConnectedOutput(), creating_input}; + command->add_child(new NodeEdgeRemoveCommand(existing_edge_to_remove.first, existing_edge_to_remove.second)); + removed_edges.push_back(existing_edge_to_remove); + } + + command->add_child(new NodeEdgeAddCommand(creating_output, creating_input)); + added_edge = {creating_output, creating_input}; + } + + creating_input.Reset(); } create_edge_dst_ = nullptr; } + // Update contexts + if (!removed_edges.empty()) { + UpdateContextsFromEdgeRemove(command, removed_edges); + } + + if (added_edge.first.IsValid()) { + UpdateContextsFromEdgeAdd(command, added_edge, removed_edges); + } + Core::instance()->undo_stack()->pushIfHasChildren(command); return; } + MultiUndoCommand* command = new MultiUndoCommand(); + if (!attached_items_.isEmpty()) { - if (attached_items_.size() == 1) { - Node* dropping_node = attached_items_.first().item->GetNode(); + if (paste_command_) { + // We've already "done" this command, but MultiUndoCommand prevents "redoing" twice, so we + // add it to this command (which may have extra commands added too) so that it all gets undone + // in the same action + command->add_child(paste_command_); + paste_command_ = nullptr; + } + } - if (drop_edge_) { - // We have everything we need to place the node in between - MultiUndoCommand* command = new MultiUndoCommand(); + { + // If any node positions changed, set them in their contexts now + MultiUndoCommand *set_pos_command = new MultiUndoCommand(); + for (auto it=positions_.begin(); it!=positions_.end(); it++) { + NodeViewItem *item = it.key(); + Position &pos_data = it.value(); + QPointF current_item_pos = item->GetNodePosition(); + Node *node = pos_data.node; - // Remove old edge - command->add_child(new NodeEdgeRemoveCommand(drop_edge_->output(), drop_edge_->input())); + if (pos_data.original_item_pos != current_item_pos) { + QPointF diff = current_item_pos - pos_data.original_item_pos; - // Place new edges - command->add_child(new NodeEdgeAddCommand(drop_edge_->output(), drop_input_)); - command->add_child(new NodeEdgeAddCommand(dropping_node, drop_edge_->input())); + for (Node *context : qAsConst(filter_nodes_)) { + if (graph_->ContextContainsNode(node, context)) { + QPointF current_node_pos_in_context = graph_->GetNodePosition(node, context); + current_node_pos_in_context += diff; + set_pos_command->add_child(new NodeSetPositionCommand(node, context, current_node_pos_in_context, false)); + } + } - Core::instance()->undo_stack()->push(command); + pos_data.original_item_pos = current_item_pos; + } + } + if (set_pos_command->child_count()) { + set_pos_command->redo(); + command->add_child(set_pos_command); + } else { + delete set_pos_command; + } + } + + + if (!attached_items_.isEmpty()) { + { + // Dropped attached item onto an edge, connect it between them + MultiUndoCommand *drop_edge_command = new MultiUndoCommand(); + if (attached_items_.size() == 1) { + Node* dropping_node = attached_items_.first().item->GetNode(); + + if (drop_edge_) { + // Remove old edge + drop_edge_command->add_child(new NodeEdgeRemoveCommand(drop_edge_->output(), drop_edge_->input())); + + // Place new edges + drop_edge_command->add_child(new NodeEdgeAddCommand(drop_edge_->output(), drop_input_)); + drop_edge_command->add_child(new NodeEdgeAddCommand(dropping_node, drop_edge_->input())); + } + + drop_edge_ = nullptr; + } + if (drop_edge_command->child_count()) { + drop_edge_command->redo(); + command->add_child(drop_edge_command); + } else { + delete drop_edge_command; + } + } + + { + // Remove from context any nodes that don't specifically output to said context + MultiUndoCommand *remove_pos_command = new MultiUndoCommand(); + + for (const AttachedItem &attached : qAsConst(attached_items_)) { + MultiUndoCommand *remove_pos_subcommand = new MultiUndoCommand(); + Node *attached_node = scene_.item_map().key(attached.item); + + bool removed = false; + QVector relevant_contexts; + for (Node *context : qAsConst(filter_nodes_)) { + if (attached_node->OutputsTo(context, true)) { + relevant_contexts.append(context); + } else { + remove_pos_subcommand->add_child(new NodeRemovePositionFromContextCommand(attached_node, context)); + removed = true; + } + } + + if (removed && !relevant_contexts.isEmpty()) { + for (Node *relevant : qAsConst(relevant_contexts)) { + remove_pos_subcommand->add_child(new NodeSetPositionCommand(attached_node, relevant, GetEstimatedPositionForContext(attached.item, relevant), false)); + } + + remove_pos_command->add_child(remove_pos_subcommand); + } else { + delete remove_pos_subcommand; + } } - drop_edge_ = nullptr; + if (remove_pos_command->child_count()) { + remove_pos_command->redo(); + command->add_child(remove_pos_command); + } else { + delete remove_pos_command; + } } DetachItemsFromCursor(); } + Core::instance()->undo_stack()->pushIfHasChildren(command); + super::mouseReleaseEvent(event); } +void NodeView::resizeEvent(QResizeEvent *event) +{ + super::resizeEvent(event); + + RepositionMiniMap(); +} + void NodeView::UpdateSelectionCache() { QVector current_selection = scene_.GetSelectedNodes(); @@ -634,7 +886,7 @@ void NodeView::UpdateSelectionCache() // All nodes in the current selection have just been selected selected = current_selection; } else { - foreach (Node* n, current_selection) { + for (Node* n : qAsConst(current_selection)) { if (!selected_nodes_.contains(n)) { selected.append(n); } @@ -646,7 +898,7 @@ void NodeView::UpdateSelectionCache() // All nodes that were selected have been deselected deselected = selected_nodes_; } else { - foreach (Node* n, selected_nodes_) { + for (Node* n : qAsConst(selected_nodes_)) { if (!current_selection.contains(n)) { deselected.append(n); } @@ -689,12 +941,6 @@ void NodeView::ShowContextMenu(const QPoint &pos) // Color menu MenuShared::instance()->AddColorCodingMenu(&m); - m.addSeparator(); - - // Auto-position action - QAction* autopos = m.addAction(tr("Auto-Position")); - connect(autopos, &QAction::triggered, this, &NodeView::AutoPositionDescendents); - ViewerOutput* viewer = dynamic_cast(selected.first()->GetNode()); if (viewer) { m.addSeparator(); @@ -718,13 +964,9 @@ void NodeView::ShowContextMenu(const QPoint &pos) Menu* filter_menu = new Menu(tr("Filter"), &m); m.addMenu(filter_menu); - filter_menu->AddActionWithData(tr("Show All"), - kFilterShowAll, - filter_mode_); + filter_menu->AddActionWithData(tr("Show All Nodes"), kFilterShowAll, filter_mode_); - filter_menu->AddActionWithData(tr("Show Selected Blocks Only"), - kFilterShowSelectedBlocks, - filter_mode_); + filter_menu->AddActionWithData(tr("Show Selected"), kFilterShowSelective, filter_mode_); connect(filter_menu, &Menu::triggered, this, &NodeView::ContextMenuFilterChanged); @@ -753,9 +995,7 @@ void NodeView::ShowContextMenu(const QPoint &pos) m.addSeparator(); - Menu* add_menu = NodeFactory::CreateMenu(&m); - add_menu->setTitle(tr("Add")); - connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot); + Menu* add_menu = CreateAddMenu(&m); m.addMenu(add_menu); } @@ -768,10 +1008,15 @@ void NodeView::CreateNodeSlot(QAction *action) Node* new_node = NodeFactory::CreateFromMenuAction(action); if (new_node) { - Core::instance()->undo_stack()->push(new NodeAddCommand(graph_, new_node)); + paste_command_ = new MultiUndoCommand(); + paste_command_->add_child(new NodeAddCommand(graph_, new_node)); + for (Node *context : qAsConst(filter_nodes_)) { + paste_command_->add_child(new NodeSetPositionCommand(new_node, context, QPointF(0, 0), false)); + } + paste_command_->add_child(new NodeViewAttachNodesToCursor(this, {new_node})); + paste_command_->redo(); - NodeViewItem* item = scene_.NodeToUIObject(new_node); - AttachItemsToCursor({item}); + this->setFocus(); } } @@ -780,18 +1025,24 @@ void NodeView::ContextMenuSetDirection(QAction *action) SetFlowDirection(static_cast(action->data().toInt())); } -void NodeView::AutoPositionDescendents() -{ - QVector selected = scene_.GetSelectedNodes(); - - foreach (Node* n, selected) { - scene_.ReorganizeFrom(n); - } -} - void NodeView::ContextMenuFilterChanged(QAction *action) { - Q_UNUSED(action) + FilterMode mode = static_cast(action->data().toInt()); + + if (filter_mode_ != mode) { + // Store temporary graph variables + NodeGraph *graph = graph_; + QVector nodes = last_set_filter_nodes_; + + // Unset graph with current filter mode + ClearGraph(); + + // Change filter mode + filter_mode_ = mode; + + // Re-set graph with new filter mode + SetGraph(graph, nodes); + } } void NodeView::OpenSelectedNodeInViewer() @@ -804,6 +1055,145 @@ void NodeView::OpenSelectedNodeInViewer() } } +// Commenting out because there shouldn't be any situations where a node would be added without +// being in a context. We're keeping RemoveNode as a fail-safe because it could provide crash +// resistance where RemoveNodePosition might be missed. +//void NodeView::AddNode(Node *node) +//{ +// if (filter_mode_ == kFilterShowAll) { +// scene_.AddNode(node); +// } +//} + +void NodeView::RemoveNode(Node *node) +{ + scene_.RemoveNode(node); +} + +void NodeView::AddEdge(const NodeOutput &output, const NodeInput &input) +{ + Node *output_node = output.node(); + Node *input_node = input.node(); + + if (scene_.item_map().contains(output_node) && scene_.item_map().contains(input_node)) { + scene_.AddEdge(output, input); + } +} + +void NodeView::RemoveEdge(const NodeOutput &output, const NodeInput &input) +{ + scene_.RemoveEdge(output, input); +} + +void NodeView::AddNodePosition(Node *node, Node *relative) +{ + bool listening_to_node = filter_nodes_.contains(relative); + + if (!listening_to_node) { + if (filter_mode_ == kFilterShowAll) { + // We're not listening to this context, but because we're showing all, add it + filter_nodes_.append(relative); + } else { + // Ignore signal + return; + } + } + + // Reposition contexts because one of their heights may have changed or a new one may have been + // added + UpdateNodeItem(node); + + if (filter_mode_ == kFilterShowAll) { + queue_reposition_contexts_ = true; + viewport()->update(); + } +} + +void NodeView::RemoveNodePosition(Node *node, Node *relative) +{ + if (filter_nodes_.contains(relative)) { + NodeViewItem *item = scene_.item_map().value(node); + + if (item && !item->GetPreventRemoving()) { + // Determine if any other contexts have this node + bool found = false; + + for (Node *context : qAsConst(filter_nodes_)) { + if (graph_->ContextContainsNode(node, context)) { + found = true; + break; + } + } + + if (!found) { + for (const Node::OutputConnection &oc : node->output_connections()) { + scene_.RemoveEdge(oc.first, oc.second); + } + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + scene_.RemoveEdge(it->second, it->first); + } + positions_.remove(item); + scene_.RemoveNode(node); + } + } + + if (filter_mode_ == kFilterShowAll) { + RepositionContexts(); + } + } +} + +void NodeView::UpdateSceneBoundingRect() +{ + // Get current items bounding rect + QRectF r = scene_.itemsBoundingRect(); + + // Adjust so that it fills the view + r.adjust(-width(), -height(), width(), height()); + + // Set it + scene_.setSceneRect(r); +} + +void NodeView::CenterOnItemsBoundingRect() +{ + centerOn(scene_.itemsBoundingRect().center()); +} + +void NodeView::RepositionMiniMap() +{ + if (minimap_->isVisible()) { + int margin = fontMetrics().height(); + + int w = width() - minimap_->width() - margin; + int h = height() - minimap_->height() - margin; + + if (verticalScrollBar()->isVisible()) { + w -= verticalScrollBar()->width(); + } + + if (horizontalScrollBar()->isVisible()) { + h -= horizontalScrollBar()->height(); + } + + minimap_->move(w, h); + + UpdateViewportOnMiniMap(); + } +} + +void NodeView::UpdateViewportOnMiniMap() +{ + if (minimap_->isVisible()) { + minimap_->SetViewportRect(mapToScene(viewport()->rect())); + } +} + +void NodeView::MoveToScenePoint(const QPointF &pos) +{ + centerOn(pos); +} + void NodeView::AttachNodesToCursor(const QVector &nodes) { QVector items(nodes.size()); @@ -820,7 +1210,7 @@ void NodeView::AttachItemsToCursor(const QVector& items) DetachItemsFromCursor(); if (!items.isEmpty()) { - foreach (NodeViewItem* i, items) { + for (NodeViewItem* i : items) { attached_items_.append({i, i->pos() - items.first()->pos()}); } @@ -842,7 +1232,7 @@ void NodeView::MoveAttachedNodesToCursor(const QPoint& p) { QPointF item_pos = mapToScene(p); - foreach (const AttachedItem& i, attached_items_) { + for (const AttachedItem& i : qAsConst(attached_items_)) { i.item->setPos(item_pos + i.original_pos); } } @@ -876,6 +1266,34 @@ void NodeView::ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, con } } +bool NodeView::event(QEvent *event) +{ + if (event->type() == QEvent::ShortcutOverride) { + QKeyEvent *se = static_cast(event); + if (se->key() == Qt::Key_Left + || se->key() == Qt::Key_Right + || se->key() == Qt::Key_Up + || se->key() == Qt::Key_Down) { + se->accept(); + return true; + } + } + + return super::event(event); +} + +bool NodeView::eventFilter(QObject *object, QEvent *event) +{ + if (object == viewport() && event->type() == QEvent::Paint) { + if (queue_reposition_contexts_) { + RepositionContexts(); + queue_reposition_contexts_ = false; + } + } + + return super::eventFilter(object, event); +} + void NodeView::ZoomFromKeyboard(double multiplier) { QPoint cursor_pos = mapFromGlobal(QCursor::pos()); @@ -888,6 +1306,296 @@ void NodeView::ZoomFromKeyboard(double multiplier) ZoomIntoCursorPosition(nullptr, multiplier, cursor_pos); } +bool NodeView::DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge) +{ + // Determines whether `node` outputs to another node in `context` besides `source` + for (const Node::OutputConnection &conn : node->output_connections()) { + Node *output_candidate = conn.second.node(); + + if (output_candidate == source) { + continue; + } + + if (graph_->ContextContainsNode(output_candidate, context)) { + if (!output_candidate->OutputsTo(source, true, removed_edges, added_edge)) { + return true; + } + } + } + + return false; +} + +void NodeView::UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Node::OutputConnections &remove_edges) +{ + // For each edge we remove, determine if we should remove the node from a context as well + for (const Node::OutputConnection &edge : remove_edges) { + Node *output_node = edge.first.node(); + QVector contexts_to_remove_from; + int contexts_containing = 0; + + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + Node *context = it.key(); + + if (it.value().contains(output_node)) { + bool currently_outputs = output_node->OutputsTo(context, true); + bool will_output_after_operation = output_node->OutputsTo(context, true, remove_edges); + + if (currently_outputs && !will_output_after_operation) { + // Will remove + contexts_to_remove_from.append(context); + } + + contexts_containing++; + } + } + + // Removing from all current contexts, convert to a floating node (i.e. don't remove from the context) + if (contexts_to_remove_from.size() != contexts_containing) { + // Not removing from all contexts, can remove + bool removing_from_all_current_contexts = true; + + for (Node *context : qAsConst(filter_nodes_)) { + if (graph_->ContextContainsNode(output_node, context)) { + if (!contexts_to_remove_from.contains(context)) { + removing_from_all_current_contexts = false; + break; + } + } + } + + for (Node *context : qAsConst(contexts_to_remove_from)) { + RecursivelyRemoveFloatingNodeFromContext(command, output_node, context, output_node, remove_edges, Node::OutputConnection(), removing_from_all_current_contexts); + } + } + } +} + +void NodeView::RecursivelyRemoveFloatingNodeFromContext(MultiUndoCommand *command, Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge, bool prevent_removing) +{ + if (prevent_removing) { + command->add_child(new NodeViewItemPreventRemovingCommand(this, node, true)); + } + + command->add_child(new NodeRemovePositionFromContextCommand(node, context)); + + // Remove any dependency from the context that's also floating + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + Node *dependency = it->second.node(); + + // Determine if this node happens to output to anything else in the context (which may be + // another floating node that won't be removed by this operation) + if (!DetermineIfNodeIsFloatingInContext(dependency, context, source, removed_edges, added_edge)) { + RecursivelyRemoveFloatingNodeFromContext(command, dependency, context, source, removed_edges, added_edge, prevent_removing); + } + } +} + +void NodeView::RecursivelyAddNodeToContext(MultiUndoCommand *command, Node *node, Node *context) +{ + command->add_child(new NodeSetPositionCommand(node, context, GetEstimatedPositionForContext(scene_.item_map().value(node), context), false)); + + // Add dependency + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + Node *dependency = it->second.node(); + RecursivelyAddNodeToContext(command, dependency, context); + } +} + +void NodeView::UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node::OutputConnection &added_edge, const Node::OutputConnections &removed_edges) +{ + // Determine if node currently does NOT output to a context that it WILL after this operation + QVector contexts_to_add_to; + Node *connecting_node = added_edge.first.node(); + Node *input_node = added_edge.second.node(); + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + if (it.value().contains(input_node)) { + contexts_to_add_to.append(it.key()); + } + } + + if (!contexts_to_add_to.isEmpty()) { + // Determine whether the node is currently "floating", i.e. it outputs to none of the contexts + // that it currently belongs to. If so, we will take ownership of it with this node. + bool node_is_floating = true; + QVector current_contexts; + for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { + if (it.value().contains(connecting_node)) { + if (connecting_node->OutputsTo(it.key(), true, removed_edges)) { + node_is_floating = false; + break; + } else { + current_contexts.append(it.key()); + } + } + } + + if (node_is_floating) { + // This action will unfloat this node, so remove it from all current contexts + for (Node *context : qAsConst(current_contexts)) { + RecursivelyRemoveFloatingNodeFromContext(command, connecting_node, context, connecting_node, removed_edges, added_edge, false); + } + } + + // Add nodes to contexts + for (Node *context : qAsConst(contexts_to_add_to)) { + RecursivelyAddNodeToContext(command, connecting_node, context); + } + } +} + +QPointF NodeView::GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const +{ + return item->GetNodePosition() - context_offsets_.value(context); +} + +Menu *NodeView::CreateAddMenu(Menu *parent) +{ + Menu* add_menu = NodeFactory::CreateMenu(parent); + add_menu->setTitle(tr("Add")); + connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot); + return add_menu; +} + +void NodeView::CreateNewEdge(NodeViewItem *output_item) +{ + create_edge_ = new NodeViewEdge(); + create_edge_src_ = output_item; + create_edge_src_output_ = Node::kDefaultOutput; + create_edge_already_exists_ = false; + + create_edge_->SetCurved(scene_.GetEdgesAreCurved()); + create_edge_->SetFlowDirection(scene_.GetFlowDirection()); + + scene_.addItem(create_edge_); +} + +void NodeView::RepositionContexts() +{ + // Determine which contexts are root-level + QVector processing_filters = filter_nodes_; + + // Level counter as we iterate through the list a few times + int level = 0; + + // Root-level positioning variables + qreal last_offset = 0; + int additional_spacing = 0; + + while (!processing_filters.isEmpty()) { + QVector contexts_on_this_level; + + for (int i=0; iContextContainsNode(context, other_context)) { + this_level = false; + break; + } + } + + if (this_level) { + contexts_on_this_level.append(context); + } + } + + for (Node *context : qAsConst(contexts_on_this_level)) { + if (level == 0) { + const NodeGraph::PositionMap &map = graph_->GetNodesForContext(context); + + // First determine the total "height" of this graph and how much we need to offset it + qreal top = 0; + qreal bottom = 0; + for (auto it=map.cbegin(); it!=map.cend(); it++) { + const QPointF &node_pos_in_context = it.value(); + top = qMin(node_pos_in_context.y(), top); + bottom = qMax(node_pos_in_context.y(), bottom); + } + + last_offset += (additional_spacing + (bottom - top)); + additional_spacing = 1; + context_offsets_.insert(context, QPointF(0, last_offset)); + } else { + // Create/update item + NodeViewItem *item = UpdateNodeItem(context, true); + + // Get position generated by UpdateNodeItem + QPointF context_pos = item->GetNodePosition(); + + // Adjust by the context node's position in its own context (this will usually be 0,0) + context_pos -= graph_->GetNodesForContext(context).value(context); + + // Insert this context's offset + context_offsets_.insert(context, context_pos); + } + + // Remove from list so we don't process again + processing_filters.removeOne(context); + } + + level++; + } + + // Now that we've positioned all the contexts, position all other nodes relative to those contexts + for (Node *context : qAsConst(filter_nodes_)) { + const NodeGraph::PositionMap &map = graph_->GetNodesForContext(context); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + UpdateNodeItem(it.key()); + } + } +} + +NodeViewItem *NodeView::UpdateNodeItem(Node *node, bool ignore_own_context) +{ + // Get UI item or create if it doesn't exist + NodeViewItem *item = scene_.item_map().value(node); + if (!item) { + item = scene_.AddNode(node); + + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + if (scene_.item_map().contains(it->second.node())) { + scene_.AddEdge(it->second, it->first); + } + } + + for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { + if (scene_.item_map().contains(it->second.node())) { + scene_.AddEdge(it->first, it->second); + } + } + } + + // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts + QPointF item_pos(DBL_MAX, 0.0); + int average_count = 0; + for (Node *context : qAsConst(filter_nodes_)) { + if (context == node && ignore_own_context) { + continue; + } + + if (graph_->GetNodesForContext(context).contains(node)) { + QPointF this_context_pos = graph_->GetNodePosition(node, context); + this_context_pos += context_offsets_.value(context); + + item_pos.setX(qMin(item_pos.x(), this_context_pos.x())); + item_pos.setY(item_pos.y() + this_context_pos.y()); + average_count++; + } + } + item_pos.setY(item_pos.y() / average_count); + + // Set position + item->SetNodePosition(item_pos); + positions_.insert(item, {node, item_pos}); + + return item; +} + NodeView::NodeViewAttachNodesToCursor::NodeViewAttachNodesToCursor(NodeView *view, const QVector &nodes) : view_(view), nodes_(nodes) @@ -910,4 +1618,23 @@ Project *NodeView::NodeViewAttachNodesToCursor::GetRelevantProject() const return dynamic_cast(view_->graph_); } +void NodeView::NodeViewItemPreventRemovingCommand::redo() +{ + NodeViewItem *item = view_->scene_.item_map().value(node_); + + if (item) { + old_prevent_removing_ = item->GetPreventRemoving(); + item->SetPreventRemoving(new_prevent_removing_); + } +} + +void NodeView::NodeViewItemPreventRemovingCommand::undo() +{ + NodeViewItem *item = view_->scene_.item_map().value(node_); + + if (item) { + item->SetPreventRemoving(old_prevent_removing_); + } +} + } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 28fb3c2d9..402c643d3 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -27,8 +27,10 @@ #include "node/graph.h" #include "node/nodecopypaste.h" #include "nodeviewedge.h" +#include "nodeviewminimap.h" #include "nodeviewscene.h" #include "widget/handmovableview/handmovableview.h" +#include "widget/menu/menu.h" namespace olive { @@ -51,10 +53,9 @@ public: return graph_; } - /** - * @brief Sets the graph to view - */ - void SetGraph(NodeGraph* graph); + void SetGraph(NodeGraph *graph, const QVector &nodes); + + void ClearGraph(); /** * @brief Delete selected nodes from graph (user-friendly/undoable) @@ -64,8 +65,8 @@ public: void SelectAll(); void DeselectAll(); - void Select(QVector nodes); - void SelectWithDependencies(QVector nodes); + void Select(QVector nodes, bool center_view_on_item); + void SelectWithDependencies(QVector nodes, bool center_view_on_item); void CopySelected(bool cut); void Paste(); @@ -78,6 +79,19 @@ public: void ZoomOut(); +public slots: + void SetMiniMapEnabled(bool e) + { + minimap_->setVisible(e); + } + + void ShowAddMenu() + { + Menu *m = CreateAddMenu(nullptr); + m->exec(QCursor::pos()); + delete m; + } + signals: void NodesSelected(const QVector& nodes); @@ -90,8 +104,14 @@ protected: virtual void mouseMoveEvent(QMouseEvent *event) override; virtual void mouseReleaseEvent(QMouseEvent* event) override; + virtual void resizeEvent(QResizeEvent *event) override; + virtual void ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, const QPointF &cursor_pos) override; + virtual bool event(QEvent *event) override; + + virtual bool eventFilter(QObject *object, QEvent *event) override; + private: void AttachNodesToCursor(const QVector &nodes); @@ -108,17 +128,32 @@ private: void ZoomFromKeyboard(double multiplier); + bool DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge); + void UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Node::OutputConnections &remove_edges); + void UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node::OutputConnection &added_edge, const Node::OutputConnections &removed_edges = Node::OutputConnections()); + void RecursivelyAddNodeToContext(MultiUndoCommand *command, Node *node, Node *context); + void RecursivelyRemoveFloatingNodeFromContext(MultiUndoCommand *command, Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge, bool prevent_removing); + + QPointF GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const; + + Menu *CreateAddMenu(Menu *parent); + + void CreateNewEdge(NodeViewItem *output_item); + + NodeViewItem *UpdateNodeItem(Node *node, bool ignore_own_context = false); + class NodeViewAttachNodesToCursor : public UndoCommand { public: NodeViewAttachNodesToCursor(NodeView* view, const QVector& nodes); + virtual Project * GetRelevantProject() const override; + + protected: virtual void redo() override; virtual void undo() override; - virtual Project * GetRelevantProject() const override; - private: NodeView* view_; @@ -126,6 +161,8 @@ private: }; + NodeViewMiniMap *minimap_; + NodeGraph* graph_; struct AttachedItem { @@ -133,6 +170,33 @@ private: QPointF original_pos; }; + class NodeViewItemPreventRemovingCommand : public UndoCommand + { + public: + NodeViewItemPreventRemovingCommand(NodeView *view, Node *node, bool prevent_removing) : + view_(view), + node_(node), + new_prevent_removing_(prevent_removing) + {} + + virtual Project * GetRelevantProject() const override + { + return node_->project(); + } + + protected: + virtual void redo() override; + + virtual void undo() override; + + private: + NodeView *view_; + Node *node_; + bool new_prevent_removing_; + bool old_prevent_removing_; + + }; + QList attached_items_; NodeViewEdge* drop_edge_; @@ -147,21 +211,34 @@ private: NodeViewScene scene_; - QVector selected_nodes_; + MultiUndoCommand* paste_command_; - QVector selected_blocks_; + QVector selected_nodes_; enum FilterMode { kFilterShowAll, - kFilterShowSelectedBlocks + kFilterShowSelective }; + struct Position { + Node *node; + QPointF original_item_pos; + }; + + QMap positions_; + FilterMode filter_mode_; + QVector filter_nodes_; + QVector last_set_filter_nodes_; + QMap context_offsets_; + double scale_; bool create_edge_already_exists_; + bool queue_reposition_contexts_; + static const double kMinimumScale; private slots: @@ -185,11 +262,6 @@ private slots: */ void ContextMenuSetDirection(QAction* action); - /** - * @brief Receiver for auto-position descendents menu action - */ - void AutoPositionDescendents(); - /** * @brief Receiver for the user changing the filter */ @@ -200,6 +272,26 @@ private slots: */ void OpenSelectedNodeInViewer(); + //void AddNode(Node *node); + void RemoveNode(Node *node); + void AddEdge(const NodeOutput& output, const NodeInput& input); + void RemoveEdge(const NodeOutput& output, const NodeInput& input); + + void AddNodePosition(Node *node, Node *relative); + void RemoveNodePosition(Node *node, Node *relative); + + void UpdateSceneBoundingRect(); + + void CenterOnItemsBoundingRect(); + + void RepositionMiniMap(); + + void UpdateViewportOnMiniMap(); + + void MoveToScenePoint(const QPointF &pos); + + void RepositionContexts(); + }; } diff --git a/app/widget/nodeview/nodeviewedge.cpp b/app/widget/nodeview/nodeviewedge.cpp index 46357aa4e..d6835ed57 100644 --- a/app/widget/nodeview/nodeviewedge.cpp +++ b/app/widget/nodeview/nodeviewedge.cpp @@ -80,80 +80,11 @@ void NodeViewEdge::SetHighlighted(bool e) void NodeViewEdge::SetPoints(const QPointF &start, const QPointF &end, bool input_is_expanded) { - QPainterPath path; - path.moveTo(start); + cached_start_ = start; + cached_end_ = end; + cached_input_is_expanded_ = input_is_expanded; - double angle = qAtan2(end.y() - start.y(), end.x() - start.x()); - - if (curved_) { - - double half_x = lerp(start.x(), end.x(), 0.5); - double half_y = lerp(start.y(), end.y(), 0.5); - - QPointF cp1, cp2; - - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { - cp1 = QPointF(half_x, start.y()); - } else { - cp1 = QPointF(start.x(), half_y); - } - - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal || input_is_expanded) { - cp2 = QPointF(half_x, end.y()); - } else { - cp2 = QPointF(end.x(), half_y); - } - - path.cubicTo(cp1, cp2, end); - - if (!qFuzzyCompare(start.x(), end.x())) { - double continue_x = end.x() - qCos(angle)*arrow_size_; - - double x1, x2, x3, x4, y1, y2, y3, y4; - if (start.x() < end.x()) { - x1 = start.x(); - x2 = cp1.x(); - x3 = cp2.x(); - x4 = end.x(); - y1 = start.y(); - y2 = cp1.y(); - y3 = cp2.y(); - y4 = end.y(); - } else { - x1 = end.x(); - x2 = cp2.x(); - x3 = cp1.x(); - x4 = start.x(); - y1 = end.y(); - y2 = cp2.y(); - y3 = cp1.y(); - y4 = start.y(); - } - - double t = Bezier::CubicXtoT(continue_x, x1, x2, x3, x4); - double y = Bezier::CubicTtoY(y1, y2, y3, y4, t); - - angle = qAtan2(end.y() - y, end.x() - continue_x); - } - - } else { - - path.lineTo(end); - - } - - setPath(path); - - const double arrow_angle = 150.0 * 3.141592 / 180.0; - QVector arrow_points(4); - arrow_points[0] = end; - arrow_points[1] = end + QPointF(qCos(angle + arrow_angle) * arrow_size_, qSin(angle + arrow_angle) * arrow_size_); - arrow_points[2] = end + QPointF(qCos(angle - arrow_angle) * arrow_size_, qSin(angle - arrow_angle) * arrow_size_); - arrow_points[3] = end; - - arrow_ = QPolygonF(arrow_points); - arrow_bounding_rect_ = arrow_.boundingRect(); - arrow_bounding_rect_.adjust(-arrow_size_, -arrow_size_, arrow_size_, arrow_size_); + UpdateCurve(); } void NodeViewEdge::SetFlowDirection(NodeViewCommon::FlowDirection dir) @@ -169,7 +100,7 @@ void NodeViewEdge::SetCurved(bool e) { curved_ = e; - update(); + UpdateCurve(); } void NodeViewEdge::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) @@ -219,4 +150,81 @@ void NodeViewEdge::Init() arrow_size_ = QFontMetrics(QFont()).height() / 2; } +void NodeViewEdge::UpdateCurve() +{ + const QPointF &start = cached_start_; + const QPointF &end = cached_end_; + const bool input_is_expanded = cached_input_is_expanded_; + + QPainterPath path; + path.moveTo(start); + + double angle = qAtan2(end.y() - start.y(), end.x() - start.x()); + + if (curved_) { + + double half_x = lerp(start.x(), end.x(), 0.5); + double half_y = lerp(start.y(), end.y(), 0.5); + + QPointF cp1, cp2; + + if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + cp1 = QPointF(half_x, start.y()); + } else { + cp1 = QPointF(start.x(), half_y); + } + + if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal || input_is_expanded) { + cp2 = QPointF(half_x, end.y()); + } else { + cp2 = QPointF(end.x(), half_y); + } + + path.cubicTo(cp1, cp2, end); + + if (!qFuzzyCompare(start.x(), end.x())) { + double continue_x = end.x() - qCos(angle)*arrow_size_; + + double x1 = start.x(); + double x2 = cp1.x(); + double x3 = cp2.x(); + double x4 = end.x(); + double y1 = start.y(); + double y2 = cp1.y(); + double y3 = cp2.y(); + double y4 = end.y(); + + if (start.x() >= end.x()) { + std::swap(x1, x4); + std::swap(x2, x3); + std::swap(y1, y4); + std::swap(y2, y3); + } + + double t = Bezier::CubicXtoT(continue_x, x1, x2, x3, x4); + double y = Bezier::CubicTtoY(y1, y2, y3, y4, t); + + angle = qAtan2(end.y() - y, end.x() - continue_x); + } + + } else { + + path.lineTo(end); + + } + + setPath(path); + + const double arrow_angle = 150.0 * M_PI / 180.0; + QVector arrow_points(4); + arrow_points[0] = end; + arrow_points[1] = end + QPointF(qCos(angle + arrow_angle) * arrow_size_, qSin(angle + arrow_angle) * arrow_size_); + arrow_points[2] = end + QPointF(qCos(angle - arrow_angle) * arrow_size_, qSin(angle - arrow_angle) * arrow_size_); + arrow_points[3] = end; + + arrow_ = QPolygonF(arrow_points); + arrow_bounding_rect_ = arrow_.boundingRect(); + arrow_bounding_rect_.adjust(-arrow_size_, -arrow_size_, arrow_size_, arrow_size_); +} + } diff --git a/app/widget/nodeview/nodeviewedge.h b/app/widget/nodeview/nodeviewedge.h index cecdf6ccd..0f90108dc 100644 --- a/app/widget/nodeview/nodeviewedge.h +++ b/app/widget/nodeview/nodeviewedge.h @@ -122,6 +122,8 @@ protected: private: void Init(); + void UpdateCurve(); + NodeOutput output_; NodeInput input_; @@ -148,6 +150,10 @@ private: QRectF arrow_bounding_rect_; + QPointF cached_start_; + QPointF cached_end_; + bool cached_input_is_expanded_; + }; } diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index ef600bb61..c3d75cb66 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -45,7 +45,8 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : expanded_(false), hide_titlebar_(false), highlighted_index_(-1), - flow_dir_(NodeViewCommon::kLeftToRight) + flow_dir_(NodeViewCommon::kLeftToRight), + prevent_removing_(false) { // Set flags for this widget setFlag(QGraphicsItem::ItemIsMovable); @@ -64,57 +65,20 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : title_bar_rect_ = QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height); setRect(title_bar_rect_); + + output_triangle_.resize(3); } QPointF NodeViewItem::GetNodePosition() const { - QPointF node_pos; - - qreal adjusted_x = pos().x() / DefaultItemHorizontalPadding(); - qreal adjusted_y = pos().y() / DefaultItemVerticalPadding(); - - switch (flow_dir_) { - case NodeViewCommon::kLeftToRight: - node_pos.setX(adjusted_x); - node_pos.setY(adjusted_y); - break; - case NodeViewCommon::kRightToLeft: - node_pos.setX(-adjusted_x); - node_pos.setY(adjusted_y); - break; - case NodeViewCommon::kTopToBottom: - node_pos.setX(adjusted_y); - node_pos.setY(adjusted_x); - break; - case NodeViewCommon::kBottomToTop: - node_pos.setX(-adjusted_y); - node_pos.setY(adjusted_x); - break; - } - - return node_pos; + return ScreenToNodePoint(pos(), flow_dir_); } void NodeViewItem::SetNodePosition(const QPointF &pos) { - switch (flow_dir_) { - case NodeViewCommon::kLeftToRight: - setPos(pos.x() * DefaultItemHorizontalPadding(), - pos.y() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kRightToLeft: - setPos(-pos.x() * DefaultItemHorizontalPadding(), - pos.y() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kTopToBottom: - setPos(pos.y() * DefaultItemHorizontalPadding(), - pos.x() * DefaultItemVerticalPadding()); - break; - case NodeViewCommon::kBottomToTop: - setPos(pos.y() * DefaultItemHorizontalPadding(), - -pos.x() * DefaultItemVerticalPadding()); - break; - } + cached_node_pos_ = pos; + + UpdateNodePosition(); } int NodeViewItem::DefaultTextPadding() @@ -129,7 +93,7 @@ int NodeViewItem::DefaultItemHeight() int NodeViewItem::DefaultItemWidth() { - return QtUtils::QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHHHHHH");; + return QtUtils::QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHHHHHHHHHH");; } int NodeViewItem::DefaultItemBorder() @@ -137,24 +101,88 @@ int NodeViewItem::DefaultItemBorder() return QFontMetrics(QFont()).height() / 12; } -qreal NodeViewItem::DefaultItemHorizontalPadding() const +QPointF NodeViewItem::NodeToScreenPoint(QPointF p, NodeViewCommon::FlowDirection direction) { - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + switch (direction) { + case NodeViewCommon::kLeftToRight: + // NodeGraphs are always left-to-right internally, no need to translate + break; + case NodeViewCommon::kRightToLeft: + // Invert X value + p.setX(-p.x()); + break; + case NodeViewCommon::kTopToBottom: + // Swap X/Y + p = QPointF(p.y(), p.x()); + break; + case NodeViewCommon::kBottomToTop: + // Swap X/Y and invert Y + p = QPointF(p.y(), -p.x()); + break; + } + + // Multiply by item sizes for this direction + p.setX(p.x() * DefaultItemHorizontalPadding(direction)); + p.setY(p.y() * DefaultItemVerticalPadding(direction)); + + return p; +} + +QPointF NodeViewItem::ScreenToNodePoint(QPointF p, NodeViewCommon::FlowDirection direction) +{ + // Divide by item sizes for this direction + p.setX(p.x() / DefaultItemHorizontalPadding(direction)); + p.setY(p.y() / DefaultItemVerticalPadding(direction)); + + switch (direction) { + case NodeViewCommon::kLeftToRight: + // NodeGraphs are always left-to-right internally, no need to translate + break; + case NodeViewCommon::kRightToLeft: + // Invert X value + p.setX(-p.x()); + break; + case NodeViewCommon::kTopToBottom: + // Swap X/Y + p = QPointF(p.y(), p.x()); + break; + case NodeViewCommon::kBottomToTop: + // Swap X/Y and invert Y + p = QPointF(-p.y(), p.x()); + break; + } + + return p; +} + +qreal NodeViewItem::DefaultItemHorizontalPadding(NodeViewCommon::FlowDirection dir) +{ + if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemWidth() * 1.5; } else { return DefaultItemWidth() * 1.25; } } -qreal NodeViewItem::DefaultItemVerticalPadding() const +qreal NodeViewItem::DefaultItemVerticalPadding(NodeViewCommon::FlowDirection dir) { - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemHeight() * 1.5; } else { return DefaultItemHeight() * 2.0; } } +qreal NodeViewItem::DefaultItemHorizontalPadding() const +{ + return DefaultItemHorizontalPadding(flow_dir_); +} + +qreal NodeViewItem::DefaultItemVerticalPadding() const +{ + return DefaultItemVerticalPadding(flow_dir_); +} + void NodeViewItem::AddEdge(NodeViewEdge *edge) { edges_.append(edge); @@ -192,8 +220,6 @@ void NodeViewItem::SetNode(Node *n) node_inputs_.append(input); } } - - SetNodePosition(node_->GetPosition()); } update(); @@ -317,6 +343,41 @@ void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti painter->setBrush(Qt::NoBrush); painter->drawRect(rect()); + + // Draw output triangle + painter->setPen(Qt::NoPen); + painter->setBrush(app_pal.color(QPalette::Text)); + int triangle_sz = title_bar_rect_.height() / 2; + int triangle_sz_half = triangle_sz / 2; + + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + // Triangle pointing right + output_triangle_[0] = QPointF(rect().right(), rect().center().y() - triangle_sz_half); + output_triangle_[1] = QPointF(rect().right() + triangle_sz_half, rect().center().y()); + output_triangle_[2] = QPointF(rect().right(), rect().center().y() + triangle_sz_half); + break; + case NodeViewCommon::kTopToBottom: + // Triangle pointing down + output_triangle_[0] = QPointF(rect().center().x() - triangle_sz_half, rect().bottom()); + output_triangle_[1] = QPointF(rect().center().x(), rect().bottom() + triangle_sz_half); + output_triangle_[2] = QPointF(rect().center().x() + triangle_sz_half, rect().bottom()); + break; + case NodeViewCommon::kBottomToTop: + // Triangle pointing up + output_triangle_[0] = QPointF(rect().center().x() - triangle_sz_half, rect().top()); + output_triangle_[1] = QPointF(rect().center().x(), rect().top() - triangle_sz_half); + output_triangle_[2] = QPointF(rect().center().x() + triangle_sz_half, rect().top()); + break; + case NodeViewCommon::kRightToLeft: + // Triangle pointing left + output_triangle_[0] = QPointF(rect().left(), rect().center().y() - triangle_sz_half); + output_triangle_[1] = QPointF(rect().left() - triangle_sz_half, rect().center().y()); + output_triangle_[2] = QPointF(rect().left(), rect().center().y() + triangle_sz_half); + break; + } + + painter->drawPolygon(output_triangle_); } void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) @@ -352,10 +413,6 @@ void NodeViewItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionHasChanged && node_) { - node_->blockSignals(true); - node_->SetPosition(GetNodePosition()); - node_->blockSignals(false); - ReadjustAllEdges(); } @@ -472,6 +529,8 @@ QPointF NodeViewItem::GetOutputPoint(const QString& output) const void NodeViewItem::SetFlowDirection(NodeViewCommon::FlowDirection dir) { flow_dir_ = dir; + + UpdateNodePosition(); } QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos) const @@ -496,4 +555,9 @@ QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos } } +void NodeViewItem::UpdateNodePosition() +{ + setPos(NodeToScreenPoint(cached_node_pos_, flow_dir_)); +} + } diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index 07703760f..a7a6854f0 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -95,8 +95,12 @@ public: static int DefaultItemBorder(); - qreal DefaultItemHorizontalPadding() const; + static QPointF NodeToScreenPoint(QPointF p, NodeViewCommon::FlowDirection direction); + static QPointF ScreenToNodePoint(QPointF p, NodeViewCommon::FlowDirection direction); + static qreal DefaultItemHorizontalPadding(NodeViewCommon::FlowDirection dir); + static qreal DefaultItemVerticalPadding(NodeViewCommon::FlowDirection dir); + qreal DefaultItemHorizontalPadding() const; qreal DefaultItemVerticalPadding() const; void AddEdge(NodeViewEdge* edge); @@ -111,6 +115,21 @@ public: void SetHighlightedIndex(int index); + void SetPreventRemoving(bool e) + { + prevent_removing_ = e; + } + + bool GetPreventRemoving() const + { + return prevent_removing_; + } + + const QPolygonF &GetOutputTriangle() const + { + return output_triangle_; + } + protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; @@ -136,6 +155,11 @@ private: */ QPointF GetInputPointInternal(int index, const QPointF &source_pos) const; + /** + * @brief Internal update function when logical position changes + */ + void UpdateNodePosition(); + /** * @brief Reference to attached Node */ @@ -167,6 +191,12 @@ private: QVector edges_; + QPointF cached_node_pos_; + + bool prevent_removing_; + + QPolygonF output_triangle_; + }; } diff --git a/app/widget/nodeview/nodeviewminimap.cpp b/app/widget/nodeview/nodeviewminimap.cpp new file mode 100644 index 000000000..d1e20791c --- /dev/null +++ b/app/widget/nodeview/nodeviewminimap.cpp @@ -0,0 +1,143 @@ +/*** + + 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 "nodeviewminimap.h" + +#include + +namespace olive { + +#define super QGraphicsView + +NodeViewMiniMap::NodeViewMiniMap(NodeViewScene *scene, QWidget *parent) : + super(parent), + resizing_(false) +{ + connect(scene, &QGraphicsScene::sceneRectChanged, this, &NodeViewMiniMap::SceneChanged); + setScene(scene); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setViewportUpdateMode(FullViewportUpdate); + setFrameShape(QFrame::Panel); + setFrameShadow(QFrame::Plain); + + QMetaObject::invokeMethod(this, &NodeViewMiniMap::SetDefaultSize, Qt::QueuedConnection); + + resize_triangle_sz_ = fontMetrics().height() / 2; +} + +void NodeViewMiniMap::SetViewportRect(const QPolygonF &rect) +{ + viewport_rect_ = rect; + + viewport()->update(); +} + +void NodeViewMiniMap::drawForeground(QPainter *painter, const QRectF &rect) +{ + super::drawForeground(painter, rect); + + QColor viewport_color = palette().text().color(); + + // Draw resize triangle + painter->save(); + painter->resetTransform(); + + QPointF triangle[3] = {QPointF(0, 0), QPointF(resize_triangle_sz_, 0), QPointF(0, resize_triangle_sz_)}; + painter->setBrush(viewport_color); + painter->setPen(viewport_color); + painter->drawPolygon(triangle, 3); + + painter->restore(); + + // Draw viewport rectangle + viewport_color.setAlphaF(0.25); + painter->setBrush(viewport_color); + + painter->drawPolygon(viewport_rect_); +} + +void NodeViewMiniMap::resizeEvent(QResizeEvent *event) +{ + super::resizeEvent(event); + + emit Resized(); + + SceneChanged(sceneRect()); +} + +void NodeViewMiniMap::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + if (event->pos().x() <= resize_triangle_sz_ && event->pos().y() <= resize_triangle_sz_) { + // Resizing! + resizing_ = true; + resize_anchor_ = QCursor::pos(); + } else { + EmitMoveSignal(event); + } + } +} + +void NodeViewMiniMap::mouseMoveEvent(QMouseEvent *event) +{ + if (event->buttons() & Qt::LeftButton) { + if (resizing_) { + QPointF movement = QCursor::pos() - resize_anchor_; + resize(QSize(width() - movement.x(), height() - movement.y())); + resize_anchor_ = QCursor::pos(); + } else { + EmitMoveSignal(event); + } + } +} + +void NodeViewMiniMap::mouseReleaseEvent(QMouseEvent *event) +{ + resizing_ = false; +} + +void NodeViewMiniMap::SceneChanged(const QRectF &bounding) +{ + double x_scale = double(this->width()) / bounding.width(); + double y_scale = double(this->height()) / bounding.height(); + + double min_scale = qMin(x_scale, y_scale); + + QTransform transform; + transform.scale(min_scale, min_scale); + + setTransform(transform); +} + +void NodeViewMiniMap::SetDefaultSize() +{ + if (parentWidget()) { + resize(parentWidget()->width()/4, parentWidget()->height()/4); + } +} + +void NodeViewMiniMap::EmitMoveSignal(QMouseEvent *event) +{ + emit MoveToScenePoint(mapToScene(event->pos())); +} + +} diff --git a/app/widget/nodeview/nodeviewminimap.h b/app/widget/nodeview/nodeviewminimap.h new file mode 100644 index 000000000..a63db1993 --- /dev/null +++ b/app/widget/nodeview/nodeviewminimap.h @@ -0,0 +1,74 @@ +/*** + + 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 NODEVIEWMINIMAP_H +#define NODEVIEWMINIMAP_H + +#include + +#include "nodeviewscene.h" + +namespace olive { + +class NodeViewMiniMap : public QGraphicsView +{ + Q_OBJECT +public: + NodeViewMiniMap(NodeViewScene *scene, QWidget *parent = nullptr); + +public slots: + void SetViewportRect(const QPolygonF &rect); + +signals: + void Resized(); + + void MoveToScenePoint(const QPointF &pos); + +protected: + virtual void drawForeground(QPainter *painter, const QRectF &rect) override; + + virtual void resizeEvent(QResizeEvent *event) override; + + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + virtual void mouseReleaseEvent(QMouseEvent *event) override; + virtual void mouseDoubleClickEvent(QMouseEvent *event) override{} + +private slots: + void SceneChanged(const QRectF &bounding); + + void SetDefaultSize(); + +private: + void EmitMoveSignal(QMouseEvent *event); + + int resize_triangle_sz_; + + QPolygonF viewport_rect_; + + bool resizing_; + + QPoint resize_anchor_; + +}; + +} + +#endif // NODEVIEWMINIMAP_H diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index d1447239a..e40db9ebe 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -43,9 +43,6 @@ void NodeViewScene::SetFlowDirection(NodeViewCommon::FlowDirection direction) QHash::const_iterator i; for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) { i.value()->SetFlowDirection(direction_); - - // Update position too - i.value()->SetNodePosition(i.key()->GetPosition()); } } @@ -68,7 +65,10 @@ void NodeViewScene::clear() // deleted. Calling this function appears to update the internal cache and prevent this. selectedItems(); - qDeleteAll(item_map_); + for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) { + DisconnectNode(it.key()); + delete it.value(); + } item_map_.clear(); qDeleteAll(edges_); @@ -150,7 +150,7 @@ QVector NodeViewScene::GetSelectedEdges() const return edges; } -void NodeViewScene::AddNode(Node* node) +NodeViewItem* NodeViewScene::AddNode(Node* node) { NodeViewItem* item = new NodeViewItem(); @@ -160,32 +160,38 @@ void NodeViewScene::AddNode(Node* node) addItem(item); item_map_.insert(node, item); - connect(node, &Node::PositionChanged, this, &NodeViewScene::NodePositionChanged); - connect(node, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); - connect(node, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); + ConnectNode(node); + + return item; } void NodeViewScene::RemoveNode(Node *node) { - disconnect(node, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); - disconnect(node, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); - disconnect(node, &Node::PositionChanged, this, &NodeViewScene::NodePositionChanged); + DisconnectNode(node); delete item_map_.take(node); } -void NodeViewScene::AddEdge(const NodeOutput &output, const NodeInput &input) +NodeViewEdge* NodeViewScene::AddEdge(const NodeOutput &output, const NodeInput &input) { - AddEdgeInternal(output, input, NodeToUIObject(output.node()), NodeToUIObject(input.node())); + NodeViewEdge *edge = EdgeToUIObject(output, input); + + if (!edge) { + edge = AddEdgeInternal(output, input, NodeToUIObject(output.node()), NodeToUIObject(input.node())); + } + + return edge; } void NodeViewScene::RemoveEdge(const NodeOutput &output, const NodeInput &input) { NodeViewEdge* edge = EdgeToUIObject(output, input); - edge->from_item()->RemoveEdge(edge); - edge->to_item()->RemoveEdge(edge); - edges_.removeOne(edge); - delete edge; + if (edge) { + edge->from_item()->RemoveEdge(edge); + edge->to_item()->RemoveEdge(edge); + edges_.removeOne(edge); + delete edge; + } } int NodeViewScene::DetermineWeight(Node *n) @@ -195,7 +201,7 @@ int NodeViewScene::DetermineWeight(Node *n) int weight = 0; foreach (Node* i, inputs) { - if (i->GetRoutesTo(n) == 1) { + if (i->GetNumberOfRoutesTo(n) == 1) { weight += DetermineWeight(i); } } @@ -203,7 +209,7 @@ int NodeViewScene::DetermineWeight(Node *n) return qMax(1, weight); } -void NodeViewScene::AddEdgeInternal(const NodeOutput& output, const NodeInput& input, NodeViewItem *from, NodeViewItem *to) +NodeViewEdge* NodeViewScene::AddEdgeInternal(const NodeOutput& output, const NodeInput& input, NodeViewItem *from, NodeViewItem *to) { NodeViewEdge* edge_ui = new NodeViewEdge(output, input, from, to); @@ -215,6 +221,20 @@ void NodeViewScene::AddEdgeInternal(const NodeOutput& output, const NodeInput& i addItem(edge_ui); edges_.append(edge_ui); + + return edge_ui; +} + +void NodeViewScene::ConnectNode(Node *n) +{ + connect(n, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); + connect(n, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); +} + +void NodeViewScene::DisconnectNode(Node *n) +{ + disconnect(n, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); + disconnect(n, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); } Qt::Orientation NodeViewScene::GetFlowOrientation() const @@ -227,39 +247,6 @@ NodeViewCommon::FlowDirection NodeViewScene::GetFlowDirection() const return direction_; } -void NodeViewScene::ReorganizeFrom(Node* n) -{ - QVector immediates = n->GetImmediateDependencies(); - - if (immediates.isEmpty()) { - // Nothing to do - return; - } - - QPointF parent_pos = n->GetPosition(); - - int weight_count = DetermineWeight(n); - - qreal child_x = parent_pos.x() - 1.0; - qreal children_height = weight_count-1; - qreal children_y = parent_pos.y() - children_height * 0.5; - - int weight_counter = 0; - - foreach (Node* i, immediates) { - if (i->GetRoutesTo(n) == 1) { - int weight = DetermineWeight(i); - - i->SetPosition(QPointF(child_x, - children_y + weight_counter + (weight - 1) * 0.5)); - - weight_counter += weight; - - ReorganizeFrom(i); - } - } -} - void NodeViewScene::SetEdgesAreCurved(bool curved) { if (curved_edges_ != curved) { @@ -271,12 +258,6 @@ void NodeViewScene::SetEdgesAreCurved(bool curved) } } -void NodeViewScene::NodePositionChanged(const QPointF &pos) -{ - // Update node's internal position - item_map_.value(static_cast(sender()))->SetNodePosition(pos); -} - void NodeViewScene::NodeAppearanceChanged() { // Force item to update diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 501e691f4..e388af140 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -79,8 +79,6 @@ public: return curved_edges_; } - void ReorganizeFrom(Node* n); - public slots: /** * @brief Slot when a Node is added to a graph (SetGraph() connects this) @@ -88,7 +86,7 @@ public slots: * This should NEVER be called directly, only connected to a NodeGraph. To add a Node to the NodeGraph * use NodeGraph::AddNode(). */ - void AddNode(Node* node); + NodeViewItem *AddNode(Node* node); /** * @brief Slot when a Node is removed from a graph (SetGraph() connects this) @@ -98,7 +96,7 @@ public slots: */ void RemoveNode(Node* node); - void AddEdge(const NodeOutput& output, const NodeInput& input); + NodeViewEdge *AddEdge(const NodeOutput& output, const NodeInput& input); void RemoveEdge(const NodeOutput& output, const NodeInput& input); /** @@ -109,7 +107,11 @@ public slots: private: static int DetermineWeight(Node* n); - void AddEdgeInternal(const NodeOutput &output, const NodeInput &input, NodeViewItem* from, NodeViewItem* to); + NodeViewEdge* AddEdgeInternal(const NodeOutput &output, const NodeInput &input, NodeViewItem* from, NodeViewItem* to); + + void ConnectNode(Node *n); + + void DisconnectNode(Node *n); QHash item_map_; @@ -122,11 +124,6 @@ private: bool curved_edges_; private slots: - /** - * @brief Receiver for whenever a node position changes - */ - void NodePositionChanged(const QPointF& pos); - /** * @brief Receiver for when a node's label has changed */ diff --git a/app/widget/nodeview/nodeviewtoolbar.cpp b/app/widget/nodeview/nodeviewtoolbar.cpp new file mode 100644 index 000000000..c63be73db --- /dev/null +++ b/app/widget/nodeview/nodeviewtoolbar.cpp @@ -0,0 +1,55 @@ +#include "nodeviewtoolbar.h" + +#include +#include + +#include "ui/icons/icons.h" + +namespace olive { + +#define super QWidget + +NodeViewToolBar::NodeViewToolBar(QWidget *parent) : + QWidget(parent) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setMargin(0); + + add_node_btn_ = new QPushButton(); + connect(add_node_btn_, &QPushButton::clicked, this, &NodeViewToolBar::AddNodeClicked); + layout->addWidget(add_node_btn_); + + minimap_btn_ = new QPushButton(); + minimap_btn_->setCheckable(true); + connect(minimap_btn_, &QPushButton::clicked, this, &NodeViewToolBar::MiniMapEnabledToggled); + layout->addWidget(minimap_btn_); + + layout->addStretch(); + + Retranslate(); + UpdateIcons(); +} + +void NodeViewToolBar::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + Retranslate(); + } else if (e->type() == QEvent::StyleChange) { + UpdateIcons(); + } + super::changeEvent(e); +} + +void NodeViewToolBar::Retranslate() +{ + add_node_btn_->setToolTip(tr("Add Node")); + minimap_btn_->setText(tr("Mini-Map")); + minimap_btn_->setToolTip(tr("Toggle Mini-Map")); +} + +void NodeViewToolBar::UpdateIcons() +{ + add_node_btn_->setIcon(icon::Add); +} + +} diff --git a/app/widget/nodeview/nodeviewtoolbar.h b/app/widget/nodeview/nodeviewtoolbar.h new file mode 100644 index 000000000..e48ed4882 --- /dev/null +++ b/app/widget/nodeview/nodeviewtoolbar.h @@ -0,0 +1,42 @@ +#ifndef NODEVIEWTOOLBAR_H +#define NODEVIEWTOOLBAR_H + +#include +#include + +namespace olive { + +class NodeViewToolBar : public QWidget +{ + Q_OBJECT +public: + NodeViewToolBar(QWidget *parent = nullptr); + +public slots: + void SetMiniMapEnabled(bool e) + { + minimap_btn_->setChecked(e); + } + +signals: + void AddNodeClicked(); + + void MiniMapEnabledToggled(bool e); + +protected: + virtual void changeEvent(QEvent *e) override; + +private: + void Retranslate(); + + void UpdateIcons(); + + QPushButton *add_node_btn_; + + QPushButton *minimap_btn_; + +}; + +} + +#endif // NODEVIEWTOOLBAR_H diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index 9723038f4..0197dcf94 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 { @@ -44,7 +43,7 @@ void NodeEdgeAddCommand::redo() remove_command_ = new NodeEdgeRemoveCommand(input_.GetConnectedOutput(), input_); } - remove_command_->redo(); + remove_command_->redo_now(); } Node::ConnectEdge(output_, input_); @@ -55,7 +54,7 @@ void NodeEdgeAddCommand::undo() Node::DisconnectEdge(output_, input_); if (remove_command_) { - remove_command_->undo(); + remove_command_->undo_now(); } } @@ -125,7 +124,7 @@ void NodeCopyInputsCommand::redo() Node::CopyInputs(src_, dest_, include_connections_); } -void NodeRemoveAndDisconnectCommand::prep() +void NodeRemoveAndDisconnectCommand::prepare() { command_ = new MultiUndoCommand(); @@ -142,6 +141,8 @@ void NodeRemoveAndDisconnectCommand::prep() for (const Node::OutputConnection& conn : node_->output_connections()) { command_->add_child(new NodeEdgeRemoveCommand(conn.first, conn.second)); } + + command_->add_child(new NodeRemovePositionFromAllContextsCommand(node_)); } void NodeRenameCommand::AddNode(Node *node, const QString &new_name) diff --git a/app/widget/nodeview/nodeviewundo.h b/app/widget/nodeview/nodeviewundo.h index 2df7613b5..c2f5aae8e 100644 --- a/app/widget/nodeview/nodeviewundo.h +++ b/app/widget/nodeview/nodeviewundo.h @@ -39,6 +39,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -61,6 +62,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -80,6 +82,7 @@ public: virtual Project* GetRelevantProject() const override; +protected: virtual void redo() override; virtual void undo() override; @@ -95,8 +98,7 @@ public: NodeRemoveAndDisconnectCommand(Node* node) : node_(node), graph_(nullptr), - command_(nullptr), - prepped_(false) + command_(nullptr) { } @@ -110,13 +112,11 @@ public: return dynamic_cast(graph_); } +protected: + virtual void prepare() override; + virtual void redo() override { - if (!prepped_) { - prep(); - prepped_ = true; - } - command_->redo(); graph_ = node_->parent(); @@ -132,8 +132,6 @@ public: } private: - void prep(); - QObject memory_manager_; Node* node_; @@ -141,16 +139,13 @@ private: MultiUndoCommand* command_; - bool prepped_; - }; class NodeRemoveWithExclusiveDependenciesAndDisconnect : public UndoCommand { public: NodeRemoveWithExclusiveDependenciesAndDisconnect(Node* node) : node_(node), - command_(nullptr), - prepped_(false) + command_(nullptr) { } @@ -168,23 +163,8 @@ public: } } - virtual void redo() override - { - if (!prepped_) { - prep(); - prepped_ = true; - } - - command_->redo(); - } - - virtual void undo() override - { - command_->undo(); - } - -private: - void prep() +protected: + virtual void prepare() override { command_ = new MultiUndoCommand(); @@ -197,9 +177,19 @@ private: } } + virtual void redo() override + { + command_->redo(); + } + + virtual void undo() override + { + command_->undo(); + } + +private: Node* node_; MultiUndoCommand* command_; - bool prepped_; }; @@ -209,12 +199,13 @@ public: Node* dest, bool include_connections); + virtual Project* GetRelevantProject() const override {return nullptr;} + +protected: virtual void redo() override; virtual void undo() override {} - virtual Project* GetRelevantProject() const override {return nullptr;} - private: const Node* src_; @@ -238,6 +229,7 @@ public: return a_->project(); } +protected: virtual void redo() override { if (link_) { @@ -278,6 +270,7 @@ public: return node_->project(); } +protected: virtual void redo() override { unlinked_ = node_->links(); @@ -334,12 +327,13 @@ public: void AddNode(Node* node, const QString& new_name); + virtual Project * GetRelevantProject() const override; + +protected: virtual void redo() override; virtual void undo() override; - virtual Project * GetRelevantProject() const override; - private: QVector nodes_; diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 49720bb0a..d653384f5 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -134,6 +134,7 @@ void ProjectExplorer::AddView(QAbstractItemView *view) view->setEditTriggers(QAbstractItemView::NoEditTriggers); connect(view, &QAbstractItemView::clicked, this, &ProjectExplorer::ItemClickedSlot); connect(view, &QAbstractItemView::doubleClicked, this, &ProjectExplorer::ItemDoubleClickedSlot); + connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ProjectExplorer::ViewSelectionChanged); connect(view, SIGNAL(DoubleClickedEmptyArea()), this, SLOT(ViewEmptyAreaDoubleClickedSlot())); stacked_widget_->addWidget(view); } @@ -509,6 +510,28 @@ void ProjectExplorer::ContextMenuStartProxy(QAction *a) } } +void ProjectExplorer::ViewSelectionChanged() +{ + QItemSelectionModel *model = static_cast(sender()); + + QModelIndexList selection = model->selectedIndexes(); + + QVector nodes; + + foreach (const QModelIndex &index, selection) { + Node *sel = static_cast(sort_model_.mapToSource(index).internalPointer()); + if (!nodes.contains(sel)) { + nodes.append(sel); + } + } + + if (nodes.isEmpty()) { + nodes.append(get_root()); + } + + emit SelectionChanged(nodes); +} + Project *ProjectExplorer::project() const { return model_.project(); diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index c9e7caf61..83010ea94 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -100,6 +100,8 @@ signals: */ void DoubleClickedItem(Node* item); + void SelectionChanged(const QVector &selected); + private: /** * @brief Get all the blocks that solely rely on an input node @@ -185,6 +187,8 @@ private slots: void ContextMenuStartProxy(QAction* a); + void ViewSelectionChanged(); + }; } diff --git a/app/widget/slider/base/numericsliderbase.cpp b/app/widget/slider/base/numericsliderbase.cpp index 38648b685..851733673 100644 --- a/app/widget/slider/base/numericsliderbase.cpp +++ b/app/widget/slider/base/numericsliderbase.cpp @@ -22,6 +22,7 @@ #include "common/qtutils.h" #include "config/config.h" +#include "core.h" namespace olive { @@ -34,7 +35,8 @@ NumericSliderBase::NumericSliderBase(QWidget *parent) : has_max_(false), dragged_diff_(0), drag_multiplier_(1.0), - setting_drag_value_(false) + setting_drag_value_(false), + is_effects_slider_(false) { // Numeric sliders are draggable, so we have a cursor that indicates that setCursor(Qt::SizeHorCursor); @@ -60,6 +62,10 @@ void NumericSliderBase::LabelPressed() connect(drag_ladder_, &SliderLadder::DraggedByValue, this, &NumericSliderBase::LadderDragged); connect(drag_ladder_, &SliderLadder::Released, this, &NumericSliderBase::LadderReleased); + + if (is_effects_slider_) { + Core::instance()->SetEffectsSliderIsBeingDragged(true); + } } void NumericSliderBase::LadderDragged(int value, double multiplier) @@ -90,6 +96,10 @@ void NumericSliderBase::LadderDragged(int value, double multiplier) void NumericSliderBase::LadderReleased() { + if (is_effects_slider_) { + Core::instance()->SetEffectsSliderIsBeingDragged(false); + } + drag_ladder_->deleteLater(); drag_ladder_ = nullptr; dragged_diff_ = 0; diff --git a/app/widget/slider/base/numericsliderbase.h b/app/widget/slider/base/numericsliderbase.h index 0f9e3df5d..3a02f49cc 100644 --- a/app/widget/slider/base/numericsliderbase.h +++ b/app/widget/slider/base/numericsliderbase.h @@ -42,6 +42,8 @@ public: bool IsDragging() const; + void SetIsEffectsSlider(bool e) {is_effects_slider_ = e;} + protected: const QVariant& GetOffset() const { @@ -87,6 +89,8 @@ private: bool setting_drag_value_; + bool is_effects_slider_; + private slots: void LabelPressed(); 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/timebased/timebasedwidget.h b/app/widget/timebased/timebasedwidget.h index 1f27300cc..e0cc79b41 100644 --- a/app/widget/timebased/timebasedwidget.h +++ b/app/widget/timebased/timebasedwidget.h @@ -148,6 +148,7 @@ private: virtual Project* GetRelevantProject() const override; + protected: virtual void redo() override; virtual void undo() override; 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 b48cdd452..000000000 --- a/app/widget/timelinewidget/timelineundo.cpp +++ /dev/null @@ -1,375 +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_, 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 - - // Determine if it's proceeded by a gap, and remove that gap if so - Block* preceding = block_->previous(); - if (dynamic_cast(preceding)) { - track_->RippleRemoveBlock(preceding); - preceding->setParent(&memory_manager_); - - existing_merged_gap_ = static_cast(preceding); - } - - // Remove block in question - track_->RippleRemoveBlock(block_); - } -} - -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 753e7d472..000000000 --- a/app/widget/timelinewidget/timelineundo.h +++ /dev/null @@ -1,2211 +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, 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), - in_(in), - out_(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, TimeRange(in_, out_)); - 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(out_, in_); - } else if (list_->type() == Track::kAudio) { - list_->parent()->ShiftAudioCache(out_, 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(in_, out_); - } else if (list_->type() == Track::kAudio) { - list_->parent()->ShiftAudioCache(in_, 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(); - } - } - } - -private: - TrackList* list_; - - QList working_tracks_; - - rational in_; - - rational out_; - - 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) - { - Init(timeline, Config::Current()[QStringLiteral("AutoMergeTracks")].toBool()); - } - - TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks) - { - Init(timeline, 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 - { - // Add track - track_->setParent(timeline_->GetParentGraph()); - timeline_->ArrayAppend(); - int track_total_index = timeline_->parent()->GetTracks().size(); - if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(track_, timeline_->parent(), track_total_index, track_total_index + 1, true); - } - position_command_->redo(); - Node::ConnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); - - // Add merge if applicable - if (merge_) { - merge_->setParent(timeline_->GetParentGraph()); - - Track* 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(); - } - } - } - - virtual void undo() override - { - // 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)); - position_command_->undo(); - timeline_->ArrayRemoveLast(); - track_->setParent(&memory_manager_); - } - -private: - void Init(TrackList* timeline, bool automerge) - { - timeline_ = timeline; - position_command_ = nullptr; - - track_ = new Track(); - track_->setParent(&memory_manager_); - - if (timeline->GetTrackCount() > 0 && automerge) { - 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_); - } - } - - TrackList* timeline_; - - Track* track_; - Node* merge_; - NodeInput base_; - NodeInput blend_; - - NodeInput direct_; - - NodeSetPositionAsChildCommand* 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 - { - // 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_); - } - - 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, gap_->index(), track->Blocks().size(), true)); - } - position_commands_.append(new NodeSetPositionAsChildCommand(insert_, 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, insert_->index(), track->Blocks().size(), true)); - } - } - - track->EndOperation(); - - if (ripple_remove_command_) { - track->Node::InvalidateCache(TimeRange(insert_->in(), insert_->out()), 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_) { - if (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 d367489e4..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" @@ -895,7 +899,7 @@ void TimelineWidget::RemoveBlock(Block *block) selected_blocks_.removeAt(select_index); RemoveSelection(block); - emit BlocksDeselected({block}); + SignalBlockSelectionChange(); } } @@ -1111,6 +1115,11 @@ void TimelineWidget::SetScrollZoomsByDefaultOnAllViews(bool e) } } +void TimelineWidget::SignalBlockSelectionChange() +{ + emit BlockSelectionChanged(selected_blocks_); +} + void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost) { ghost_items_.append(ghost); @@ -1192,7 +1201,7 @@ void TimelineWidget::SignalSelectedBlocks(QVector input, bool filter) selected_blocks_.append(input); - emit BlocksSelected(input); + emit SignalBlockSelectionChange(); } void TimelineWidget::SignalDeselectedBlocks(const QVector &deselected_blocks) @@ -1205,14 +1214,14 @@ void TimelineWidget::SignalDeselectedBlocks(const QVector &deselected_b selected_blocks_.removeOne(b); } - emit BlocksDeselected(deselected_blocks); + emit SignalBlockSelectionChange(); } void TimelineWidget::SignalDeselectedAllBlocks() { if (!selected_blocks_.isEmpty()) { - emit BlocksDeselected(selected_blocks_); selected_blocks_.clear(); + SignalBlockSelectionChange(); } } diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 55cf634eb..971e57866 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -214,6 +214,9 @@ public: { } + virtual Project* GetRelevantProject() const override {return nullptr;} + + protected: virtual void redo() override { timeline_->SetSelections(now_); @@ -224,8 +227,6 @@ public: timeline_->SetSelections(old_); } - virtual Project* GetRelevantProject() const override {return nullptr;} - private: TimelineWidget* timeline_; TimelineWidgetSelections old_; @@ -234,9 +235,7 @@ public: }; signals: - void BlocksSelected(const QVector& selected_blocks); - - void BlocksDeselected(const QVector& deselected_blocks); + void BlockSelectionChanged(const QVector& selected_blocks); protected: virtual void resizeEvent(QResizeEvent *event) override; @@ -355,6 +354,8 @@ private slots: void SetScrollZoomsByDefaultOnAllViews(bool e); + void SignalBlockSelectionChange(); + }; } diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index bd766735a..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 { @@ -108,15 +109,14 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) NodeGraph* graph = static_cast(parent()->GetConnectedNode()->parent()); - command->add_child(new NodeAddCommand(graph, - clip)); - + command->add_child(new NodeAddCommand(graph, clip)); + command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0), false)); command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(track.type()), track.index(), clip, ghost_->GetAdjustedIn())); - QPointF extra_node_offset(-1, 0); + Node *node_to_add = nullptr; switch (Core::instance()->GetSelectedAddableObject()) { case olive::Tool::kAddableEmpty: @@ -124,24 +124,12 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) break; case olive::Tool::kAddableSolid: { - Node* solid = new SolidGenerator(); - - command->add_child(new NodeAddCommand(graph, - solid)); - - command->add_child(new NodeEdgeAddCommand(solid, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(solid, clip, extra_node_offset)); + node_to_add = new SolidGenerator(); break; } case olive::Tool::kAddableTitle: { - Node* text = new TextGenerator(); - - command->add_child(new NodeAddCommand(graph, - text)); - - command->add_child(new NodeEdgeAddCommand(text, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(text, clip, extra_node_offset)); + node_to_add = new TextGenerator(); break; } case olive::Tool::kAddableBars: @@ -157,6 +145,13 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) break; } + if (node_to_add) { + QPointF extra_node_offset(-1, 0); + command->add_child(new NodeAddCommand(graph, node_to_add)); + command->add_child(new NodeEdgeAddCommand(node_to_add, NodeInput(clip, ClipBlock::kBufferIn))); + command->add_child(new NodeSetPositionCommand(node_to_add, clip, extra_node_offset, false)); + } + Core::instance()->undo_stack()->push(command); } diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 19d2c1f3f..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" @@ -353,6 +354,7 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeAddCommand(dst_graph, new_sequence)); command->add_child(new FolderAddChild(Core::instance()->GetSelectedFolderInActiveProject(), new_sequence)); + command->add_child(new NodeSetPositionCommand(new_sequence, new_sequence, QPointF(0, 0), false)); new_sequence->add_default_nodes(command); FootageToGhosts(0, dragged_footage_, new_sequence->GetVideoParams().time_base(), 0); @@ -391,7 +393,12 @@ void ImportTool::DropGhosts(bool insert) clip->set_length_and_media_out(ghost->GetLength()); clip->SetLabel(footage_stream.footage->GetLabel()); command->add_child(new NodeAddCommand(dst_graph, clip)); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(clip, footage_stream.footage, QPointF(2, 0))); + + // Position clip in its own context + command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0), false)); + + // Position footage in its context + command->add_child(new NodeSetPositionCommand(footage_stream.footage, clip, QPointF(-2, 0), false)); switch (Track::Reference::TypeFromString(footage_stream.output)) { case Track::kVideo: @@ -401,7 +408,7 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeEdgeAddCommand(corresponding_output, NodeInput(transform, TransformDistortNode::kTextureInput))); command->add_child(new NodeEdgeAddCommand(transform, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(transform, clip, QPointF(-1, 0))); + command->add_child(new NodeSetPositionCommand(transform, clip, QPointF(-1, 0), false)); break; } case Track::kAudio: @@ -411,7 +418,7 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeEdgeAddCommand(corresponding_output, NodeInput(volume_node, VolumeNode::kSamplesInput))); command->add_child(new NodeEdgeAddCommand(volume_node, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionToOffsetOfAnotherNodeCommand(volume_node, clip, QPointF(-1, 0))); + command->add_child(new NodeSetPositionCommand(volume_node, clip, QPointF(-1, 0), false)); break; } default: 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..3a0d28f19 --- /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_now(); + 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..e9b2cda64 --- /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_now(); + } +} + +void TransitionRemoveCommand::undo() +{ + if (remove_from_graph_) { + remove_command_->undo_now(); + } + + track_->BeginOperation(); + + if (in_block_) { + track_->InsertBlockBefore(block_, in_block_); + } else { + track_->InsertBlockAfter(block_, out_block_); + } + + if (in_block_) { + Node::ConnectEdge(in_block_, NodeInput(block_, TransitionBlock::kInBlockInput)); + } + + if (out_block_) { + Node::ConnectEdge(out_block_, NodeInput(block_, TransitionBlock::kOutBlockInput)); + } + + // These if statements must be separated because in_offset and out_offset report different things + // if only one block is connected vs two. So we have to connect the blocks first before we have + // an accurate return value from these offset functions. + if (in_block_) { + in_block_->set_length_and_media_in(in_block_->length() - block_->in_offset()); + } + + if (out_block_) { + out_block_->set_length_and_media_out(out_block_->length() - block_->out_offset()); + } + + track_->EndOperation(); + + track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput); +} + +// +// TrackListInsertGaps +// +void TrackListInsertGaps::prepare() +{ + // Determine if all tracks will be affected, which will allow us to make some optimizations + all_tracks_unlocked_ = true; + + foreach (Track* track, track_list_->GetTracks()) { + if (track->IsLocked()) { + all_tracks_unlocked_ = false; + continue; + } + + working_tracks_.append(track); + } + + QVector blocks_to_split; + QVector blocks_to_append_gap_to; + QVector 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_now(); + } + + 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_now(); + } + + 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_now(); + + } 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_now(); + } +} + +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..b47587ff5 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundogeneral.h @@ -0,0 +1,333 @@ +/*** + + 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(); + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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..0954cd41c --- /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_now(); + } + } + } 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_now(); + } + + 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_now(); + } + } 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_now(); + } + } 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_now(); + } 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_now(); + } 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_now(); + } + } + + 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_now(); + 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_now(); + } +} + +void TrackPlaceBlockCommand::undo() +{ + for (int i=position_commands_.size()-1; i>=0; i--) { + position_commands_.at(i)->undo_now(); + } + + 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_now(); + } 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_now(); + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundopointer.h b/app/widget/timelinewidget/undo/timelineundopointer.h new file mode 100644 index 000000000..fc7cf6566 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundopointer.h @@ -0,0 +1,207 @@ +/*** + + 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; + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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..4d8ff9fb3 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoripple.cpp @@ -0,0 +1,503 @@ +/*** + + 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_now(); + } + } + } + + 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_now(); + } + + 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_now(); + } + + 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_now(); + } + + 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); + } else if (pre_latest_out < post_latest_out) { + // If we're here, then a new section has been rippled in that needs to be rendered + track->Node::InvalidateCache(TimeRange(pre_latest_out, post_latest_out), 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_now(); + } +} + +} diff --git a/app/widget/timelinewidget/undo/timelineundoripple.h b/app/widget/timelinewidget/undo/timelineundoripple.h new file mode 100644 index 000000000..9164ce9f3 --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoripple.h @@ -0,0 +1,241 @@ +/*** + + 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; + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + virtual void redo() override; + + virtual void undo() override + { + for (int i=commands_.size()-1;i>=0;i--) { + commands_.at(i)->undo_now(); + } + } + +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..637b1e0a7 --- /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, track, new_block()->index(), track->Blocks().size(), true); + } + position_command_->redo_now(); + + // 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_now(); + + 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_now(); + commands_.append(blc); + } + } + } + } + } else { + for (int i=0; iredo_now(); + } + } +} + +// +// 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..82b57ebca --- /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_now(); + } + } + +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_now(); + } + } + + virtual void undo() override + { + if (command_) { + command_->undo_now(); + } + } + +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..ca2587b5a --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundotrack.h @@ -0,0 +1,167 @@ +/*** + + 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(); + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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(); + } + +protected: + 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..c431178ec --- /dev/null +++ b/app/widget/timelinewidget/undo/timelineundoworkarea.h @@ -0,0 +1,105 @@ +/*** + + 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_; + } + +protected: + 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_; + } + +protected: + 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/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index 36bca33a2..ccadef51b 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -69,7 +69,7 @@ void TimeRuler::SetPlaybackCache(PlaybackCache *cache) if (playback_cache_) { disconnect(playback_cache_, &PlaybackCache::Invalidated, this, static_cast(&TimeRuler::update)); disconnect(playback_cache_, &PlaybackCache::Validated, this, static_cast(&TimeRuler::update)); - disconnect(playback_cache_, &PlaybackCache::LengthChanged, this, static_cast(&TimeRuler::update)); + disconnect(playback_cache_, &PlaybackCache::Shifted, this, static_cast(&TimeRuler::update)); } playback_cache_ = cache; @@ -77,7 +77,7 @@ void TimeRuler::SetPlaybackCache(PlaybackCache *cache) if (playback_cache_) { connect(playback_cache_, &PlaybackCache::Invalidated, this, static_cast(&TimeRuler::update)); connect(playback_cache_, &PlaybackCache::Validated, this, static_cast(&TimeRuler::update)); - connect(playback_cache_, &PlaybackCache::LengthChanged, this, static_cast(&TimeRuler::update)); + connect(playback_cache_, &PlaybackCache::Shifted, this, static_cast(&TimeRuler::update)); } update(); @@ -254,14 +254,17 @@ void TimeRuler::paintEvent(QPaintEvent *) // If cache status is enabled if (show_cache_status_ && playback_cache_) { - int cache_screen_length = qMin(TimeToScreen(playback_cache_->GetLength()), width()); + // FIXME: Hardcoded to get video length, if we ever need audio length, this will have to change + rational len = playback_cache_->viewer_parent()->GetVideoLength(); + + int cache_screen_length = qMin(TimeToScreen(len), width()); if (cache_screen_length > 0) { int cache_y = height() - cache_status_height_; p.fillRect(0, cache_y, cache_screen_length , cache_status_height_, Qt::green); - foreach (const TimeRange& range, playback_cache_->GetInvalidatedRanges()) { + foreach (const TimeRange& range, playback_cache_->GetInvalidatedRanges(len)) { int range_left = TimeToScreen(range.in()); if (range_left >= width()) { continue; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index fe474d4a6..736450acf 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -52,7 +52,6 @@ const int kMaxPreQueueSize = 8; ViewerWidget::ViewerWidget(QWidget *parent) : super(false, true, parent), playback_speed_(0), - frame_cache_job_time_(0), color_menu_enabled_(true), time_changed_from_timer_(false), prequeuing_(false), @@ -453,7 +452,7 @@ void ViewerWidget::StartAudioOutput() if (audio_cache->GetParameters().is_valid()) { AudioManager::instance()->SetOutputParams(audio_cache->GetParameters()); AudioManager::instance()->StartOutput(audio_cache, - audio_cache->GetParameters().time_to_bytes(GetTime()), + audio_cache->GetParameters().time_to_bytes_per_channel(GetTime()), playback_speed_); emit AudioManager::instance()->OutputWaveformStarted(&audio_cache->visual(), GetTime(), playback_speed_); @@ -645,7 +644,7 @@ void ViewerWidget::PushScrubbedAudio() int size_of_sample = params.time_to_bytes(rational(20, 1000)); // Push audio - audio_src->seek(params.time_to_bytes(GetTime())); + audio_src->seek(params.time_to_bytes_per_channel(GetTime())); QByteArray frame_audio = audio_src->read(size_of_sample); AudioManager::instance()->SetOutputParams(params); AudioManager::instance()->PushToOutput(frame_audio); diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 8b0f93a1a..402164ee3 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -215,8 +215,6 @@ private: QAtomicInt playback_speed_; - qint64 frame_cache_job_time_; - int64_t last_time_; bool color_menu_enabled_; diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 297de30c7..7db76d1ba 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -95,7 +95,9 @@ MainWindow::MainWindow(QWidget *parent) : connect(node_panel_, &NodePanel::NodesDeselected, param_panel_, &ParamPanel::DeselectNodes); connect(node_panel_, &NodePanel::NodesSelected, table_panel_, &NodeTablePanel::SelectNodes); connect(node_panel_, &NodePanel::NodesDeselected, table_panel_, &NodeTablePanel::DeselectNodes); - connect(param_panel_, &ParamPanel::RequestSelectNode, node_panel_, &NodePanel::Select); + connect(param_panel_, &ParamPanel::RequestSelectNode, this, [this](const QVector& target){ + node_panel_->Select(target, true); + }); connect(param_panel_, &ParamPanel::FocusedNodeChanged, sequence_viewer_panel_, &ViewerPanel::SetGizmos); // Connect time signals together @@ -385,7 +387,7 @@ void MainWindow::ProjectClose(Project *p) // Close project from NodeView if (node_panel_->GetGraph() == p) { - node_panel_->SetGraph(nullptr); + node_panel_->ClearGraph(); } } @@ -466,6 +468,24 @@ void MainWindow::StatusBarDoubleClicked() task_man_panel_->raise(); } +void MainWindow::TimelinePanelSelectionChanged(const QVector &blocks) +{ + TimelinePanel *panel = static_cast(sender()); + + if (PanelManager::instance()->CurrentlyFocused(false) == panel) { + UpdateNodePanelContextFromTimelinePanel(panel); + } +} + +void MainWindow::ProjectPanelSelectionChanged(const QVector &nodes) +{ + ProjectPanel *panel = static_cast(sender()); + + if (PanelManager::instance()->CurrentlyFocused(false) == panel) { + node_panel_->Select(nodes, true); + } +} + #ifdef Q_OS_LINUX void MainWindow::ShowNouveauWarning() { @@ -539,8 +559,7 @@ TimelinePanel* MainWindow::AppendTimelinePanel() connect(panel, &TimelinePanel::TimeChanged, param_panel_, &ParamPanel::SetTimestamp); connect(panel, &TimelinePanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp); connect(panel, &TimelinePanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTimestamp); - connect(panel, &TimelinePanel::BlocksSelected, node_panel_, &NodePanel::SelectBlocks); - connect(panel, &TimelinePanel::BlocksDeselected, node_panel_, &NodePanel::DeselectBlocks); + connect(panel, &TimelinePanel::BlockSelectionChanged, this, &MainWindow::TimelinePanelSelectionChanged); connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); connect(curve_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); @@ -556,6 +575,7 @@ ProjectPanel *MainWindow::AppendProjectPanel() connect(panel, &PanelWidget::CloseRequested, this, &MainWindow::ProjectCloseRequested); connect(panel, &ProjectPanel::ProjectNameChanged, this, &MainWindow::UpdateTitle); + connect(panel, &ProjectPanel::SelectionChanged, this, &MainWindow::ProjectPanelSelectionChanged); return panel; } @@ -697,27 +717,53 @@ void MainWindow::UpdateAudioMonitorParams(ViewerOutput *viewer) } } +void MainWindow::UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel) +{ + // Add selected blocks (if any) + const QVector &blocks = panel->GetSelectedBlocks(); + QVector context(blocks.size()); + for (int i=0; iGetConnectedViewer(); + if (viewer && context.isEmpty()) { + context.append(viewer); + } + + node_panel_->SetGraph(viewer ? viewer->parent() : nullptr, context); + if (viewer) { + node_panel_->SelectWithDependencies(context, false); + } +} + void MainWindow::FocusedPanelChanged(PanelWidget *panel) { // Update audio monitor panel - TimeBasedPanel* tbp = dynamic_cast(panel); - if (tbp) { + if (TimeBasedPanel* tbp = dynamic_cast(panel)) { UpdateAudioMonitorParams(tbp->GetConnectedViewer()); } - // Signal timeline focus - TimelinePanel* timeline = dynamic_cast(panel); - if (timeline) { + if (TimelinePanel* timeline = dynamic_cast(panel)) { + // Signal timeline focus TimelineFocused(timeline->GetConnectedViewer()); - return; - } - // Signal project panel focus - ProjectPanel* project = dynamic_cast(panel); - if (project) { + UpdateNodePanelContextFromTimelinePanel(timeline); + } else if (ProjectPanel* project = dynamic_cast(panel)) { + // Signal project panel focus UpdateTitle(); - node_panel_->SetGraph(project->project()); - return; + if (project->project()) { + node_panel_->SetGraph(project->project(), {project->project()->root()}); + + bool center = true; + auto selected = project->SelectedItems(); + if (selected.isEmpty()) { + selected.append(project->project()->root()); + center = false; + } + node_panel_->Select(selected, center); + } } } diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index 18c54f401..0e292571c 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -142,6 +142,8 @@ private: void UpdateAudioMonitorParams(ViewerOutput* viewer); + void UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel); + QByteArray premaximized_state_; // Standard panels @@ -190,6 +192,10 @@ private slots: void ShowNouveauWarning(); #endif + void TimelinePanelSelectionChanged(const QVector &blocks); + + void ProjectPanelSelectionChanged(const QVector &nodes); + }; } diff --git a/app/window/mainwindow/mainwindowundo.h b/app/window/mainwindow/mainwindowundo.h index 6e048f283..51bc52c64 100644 --- a/app/window/mainwindow/mainwindowundo.h +++ b/app/window/mainwindow/mainwindowundo.h @@ -32,12 +32,13 @@ public: sequence_(sequence) {} + virtual Project* GetRelevantProject() const override {return nullptr;} + +protected: virtual void redo() override; virtual void undo() override; - virtual Project* GetRelevantProject() const override {return nullptr;} - private: Sequence* sequence_; @@ -50,12 +51,13 @@ public: sequence_(sequence) {} + virtual Project* GetRelevantProject() const override {return nullptr;} + +protected: virtual void redo() override; virtual void undo() override; - virtual Project* GetRelevantProject() const override {return nullptr;} - private: Sequence* sequence_; diff --git a/tests/general/CMakeLists.txt b/tests/general/CMakeLists.txt index 11f3472ed..7f87c6302 100644 --- a/tests/general/CMakeLists.txt +++ b/tests/general/CMakeLists.txt @@ -16,3 +16,4 @@ olive_add_test(General common-tests common-tests.cpp) olive_add_test(General rational-tests rational-tests.cpp) +olive_add_test(General timerange-tests timerange-tests.cpp) diff --git a/tests/general/timerange-tests.cpp b/tests/general/timerange-tests.cpp new file mode 100644 index 000000000..b35d70102 --- /dev/null +++ b/tests/general/timerange-tests.cpp @@ -0,0 +1,108 @@ +/*** + + 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 "testutil.h" + +#include "common/timerange.h" + +namespace olive { + +OLIVE_ADD_TEST(TimeRangeListMergeAdjacent) +{ + TimeRangeList t; + + // TimeRangeList should merge 1 and 3 together since they're adjacent + t.insert(TimeRange(0, 6)); + t.insert(TimeRange(20, 30)); + t.insert(TimeRange(6, 10)); + + OLIVE_ASSERT(t.size() == 2); + OLIVE_ASSERT(t.first() == TimeRange(20, 30)); + OLIVE_ASSERT(t.at(1) == TimeRange(0, 10)); + + // TimeRangeList should ignore these because it's already contained + TimeRangeList noop_test = t; + + noop_test.insert(TimeRange(4, 7)); + OLIVE_ASSERT(noop_test == t); + + noop_test.insert(TimeRange(0, 3)); + OLIVE_ASSERT(noop_test == t); + + noop_test.insert(TimeRange(25, 30)); + OLIVE_ASSERT(noop_test == t); + + // TimeRangeList should combine all these together + TimeRangeList combine_test_no_overlap = t; + combine_test_no_overlap.insert(TimeRange(10, 20)); + OLIVE_ASSERT(combine_test_no_overlap.size() == 1); + OLIVE_ASSERT(combine_test_no_overlap.first() == TimeRange(0, 30)); + + TimeRangeList combine_test_in_overlap = t; + combine_test_in_overlap.insert(TimeRange(9, 20)); + OLIVE_ASSERT(combine_test_in_overlap.size() == 1); + OLIVE_ASSERT(combine_test_in_overlap.first() == TimeRange(0, 30)); + + TimeRangeList combine_test_out_overlap = t; + combine_test_out_overlap.insert(TimeRange(10, 21)); + OLIVE_ASSERT(combine_test_out_overlap.size() == 1); + OLIVE_ASSERT(combine_test_out_overlap.first() == TimeRange(0, 30)); + + TimeRangeList combine_test_both_overlap = t; + combine_test_both_overlap.insert(TimeRange(9, 21)); + OLIVE_ASSERT(combine_test_both_overlap.size() == 1); + OLIVE_ASSERT(combine_test_both_overlap.first() == TimeRange(0, 30)); + + OLIVE_TEST_END; +} + +OLIVE_ADD_TEST(TimeRangeListFrameIteratorSize) +{ + const rational timebase(1, 10); + + TimeRangeList ranges; + + ranges.insert(TimeRange(0, 10)); // 100 + ranges.insert(TimeRange(25, 30)); // 50 + ranges.insert(TimeRange(50, 60)); // 100 + ranges.insert(TimeRange(70, rational(1401, 20))); // 1 + ranges.insert(TimeRange(rational(1402, 20), rational(1403, 20))); // 1 + ranges.insert(TimeRange(rational(10001, 40), rational(10002, 40))); // 0 + ranges.insert(TimeRange(rational(10001, 40), rational(10004, 40))); // 0 + ranges.insert(TimeRange(rational(10001, 40), rational(10005, 40))); // 1 + + TimeRangeListFrameIterator iterator(ranges, timebase); + + QVector vec = iterator.ToVector(); + + OLIVE_ASSERT_EQUAL(vec.size(), 253); + OLIVE_ASSERT_EQUAL(iterator.size(), vec.size()); + + TimeRangeListFrameIterator empty(TimeRangeList(), timebase); + + QVector empty_vec = empty.ToVector(); + + OLIVE_ASSERT_EQUAL(empty_vec.size(), 0); + OLIVE_ASSERT_EQUAL(empty_vec.size(), empty.size()); + + OLIVE_TEST_END; +} + +} diff --git a/tests/testutil.h b/tests/testutil.h index 9f916c2d0..3cfd6845d 100644 --- a/tests/testutil.h +++ b/tests/testutil.h @@ -21,6 +21,7 @@ #include #define OLIVE_ASSERT(x) if (!(x)) return false +#define OLIVE_ASSERT_EQUAL(x, y) if (x != y) {std::cout << " - Equal assert failed on line " << __LINE__ << ": " << x << " != " << y; return false;}void() #define OLIVE_TEST_END return true #define OLIVE_ADD_TEST(x) bool Test##x() diff --git a/tests/timeline/timeline-tests.cpp b/tests/timeline/timeline-tests.cpp index 529168e30..0f0c266b9 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 { @@ -134,14 +135,14 @@ OLIVE_ADD_TEST(Trim) { // Trim out point of second block BlockTrimCommand command(track, block2, 1, Timeline::kTrimOut); - command.redo(); + command.redo_now(); // No block should have been added OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 1); OLIVE_ASSERT(block1->length() == 2); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); @@ -151,7 +152,7 @@ OLIVE_ADD_TEST(Trim) { // Trim in point of second block BlockTrimCommand command(track, block2, 1, Timeline::kTrimIn); - command.redo(); + command.redo_now(); // Gap should be inserted in between OLIVE_ASSERT(track->Blocks().size() == 3); @@ -163,7 +164,7 @@ OLIVE_ADD_TEST(Trim) OLIVE_ASSERT(block1->next() == gap); OLIVE_ASSERT(block2->previous() == gap); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); @@ -173,7 +174,7 @@ OLIVE_ADD_TEST(Trim) { // Trim out point of first block BlockTrimCommand command(track, block1, 1, Timeline::kTrimOut); - command.redo(); + command.redo_now(); // Gap should be inserted in between OLIVE_ASSERT(track->Blocks().size() == 3); @@ -185,7 +186,7 @@ OLIVE_ADD_TEST(Trim) OLIVE_ASSERT(block1->next() == gap); OLIVE_ASSERT(block2->previous() == gap); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); @@ -195,7 +196,7 @@ OLIVE_ADD_TEST(Trim) { // Trim in point of first block BlockTrimCommand command(track, block1, 1, Timeline::kTrimIn); - command.redo(); + command.redo_now(); // Gap should be prepended to the start OLIVE_ASSERT(track->Blocks().size() == 3); @@ -207,7 +208,7 @@ OLIVE_ADD_TEST(Trim) OLIVE_ASSERT(block1->next() == block2); OLIVE_ASSERT(block1->previous() == gap); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 2); OLIVE_ASSERT(block2->length() == 2); @@ -240,7 +241,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsOnly) { // Replace clip C with a gap TrackReplaceBlockWithGapCommand command(track, c); - command.redo(); + command.redo_now(); // Clip should be removed without any gap actually taking its place, since the clip is at the // end of the track @@ -248,7 +249,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsOnly) OLIVE_ASSERT(track->Blocks().at(0) == a); OLIVE_ASSERT(track->Blocks().at(1) == b); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -259,7 +260,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsOnly) { // Replace clip B with a gap TrackReplaceBlockWithGapCommand command(track, b); - command.redo(); + command.redo_now(); // B should be replaced with a gap OLIVE_ASSERT(track->Blocks().size() == 3); @@ -269,7 +270,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsOnly) OLIVE_ASSERT(track->Blocks().at(1)->length() == b->length()); OLIVE_ASSERT(track->Blocks().at(2) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -311,7 +312,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) { // Replace clip E with a gap TrackReplaceBlockWithGapCommand command(track, e); - command.redo(); + command.redo_now(); // Both clips D and E should be removed because this command should remove any trailing gaps OLIVE_ASSERT(track->Blocks().size() == 3); @@ -320,7 +321,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) OLIVE_ASSERT(track->Blocks().at(2) == c); // Test undo - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -336,7 +337,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) rational original_length_of_b = b->length(); TrackReplaceBlockWithGapCommand command(track, a); - command.redo(); + command.redo_now(); // A should be removed and B should take its place OLIVE_ASSERT(track->Blocks().size() == 4); @@ -348,7 +349,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) OLIVE_ASSERT(b->length() == original_length_of_a + original_length_of_b); // Test undo - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -367,7 +368,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) rational original_length_of_d = d->length(); TrackReplaceBlockWithGapCommand command(track, c); - command.redo(); + command.redo_now(); // C and D should be removed, and B should take both of their places OLIVE_ASSERT(track->Blocks().size() == 3); @@ -377,7 +378,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) OLIVE_ASSERT(b->length() == original_length_of_b + original_length_of_c + original_length_of_d); // Test undo - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -400,7 +401,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) TrackReplaceBlockWithGapCommand command(track, e); rational original_length_of_d = d->length(); rational original_length_of_e = e->length(); - command.redo(); + command.redo_now(); // E should be removed and D should have taken its place OLIVE_ASSERT(track->Blocks().size() == 5); @@ -411,7 +412,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndGaps) OLIVE_ASSERT(track->Blocks().at(4) == f); OLIVE_ASSERT(d->length() == original_length_of_d + original_length_of_e); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 6); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -465,7 +466,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndTransitions) { // Replace A with gap TrackReplaceBlockWithGapCommand command(track, a); - command.redo(); + command.redo_now(); // A should be replaced with a gap and so should A_IN since A was the only clip connected to it. // Also A_TO_B should only be connected to B now @@ -475,7 +476,7 @@ OLIVE_ADD_TEST(ReplaceBlockWithGap_ClipsAndTransitions) OLIVE_ASSERT(track->Blocks().at(2) == b); OLIVE_ASSERT(track->Blocks().at(3) == b_out); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a_in); @@ -517,7 +518,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) { // Insert gap at the start of the track, all blocks should be unsplit and shifted to the right TrackListInsertGaps command(list, 0, 2); - command.redo(); + command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 4); OLIVE_ASSERT(dynamic_cast(track->Blocks().at(0))); @@ -526,7 +527,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) OLIVE_ASSERT(track->Blocks().at(2) == b); OLIVE_ASSERT(track->Blocks().at(3) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -537,7 +538,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) { // Insert gap in the middle of block A, block A should be halved with a copy at 2 and the gap at 1 TrackListInsertGaps command(list, rational(1, 2), 2); - command.redo(); + command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 5); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -547,7 +548,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) OLIVE_ASSERT(track->Blocks().at(3) == b); OLIVE_ASSERT(track->Blocks().at(4) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -559,7 +560,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) { // Insert gap between block A and B, blocks should be unsplit with a gap at 1 TrackListInsertGaps command(list, 1, 2); - command.redo(); + command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 4); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -567,7 +568,7 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) OLIVE_ASSERT(track->Blocks().at(2) == b); OLIVE_ASSERT(track->Blocks().at(3) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); @@ -578,14 +579,14 @@ OLIVE_ADD_TEST(InsertGaps_SingleTrack) { // Insert gap at end, nothing should be added TrackListInsertGaps command(list, 3, 2); - command.redo(); + command.redo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a); OLIVE_ASSERT(track->Blocks().at(1) == b); OLIVE_ASSERT(track->Blocks().at(2) == c); - command.undo(); + command.undo_now(); OLIVE_ASSERT(track->Blocks().size() == 3); OLIVE_ASSERT(track->Blocks().at(0) == a);