From 2bda306866521bd39fc1635f32f75bcd3951203a Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 31 Mar 2020 02:19:05 +1100 Subject: [PATCH] timeline/various: implemented block copy/paste Implemented the ability to copy/paste blocks/clips in the timeline. This did require some large scale changes and reworking of the copy/paste system introduced a few commits ago, but should be largely functional now. --- app/common/xmlutils.cpp | 23 ++- app/common/xmlutils.h | 27 +++- app/core.cpp | 97 +---------- app/core.h | 4 +- app/node/block/block.cpp | 1 + app/node/input.cpp | 26 ++- app/node/input.h | 6 +- app/node/inputarray.cpp | 6 +- app/node/inputarray.h | 2 +- app/node/node.cpp | 10 +- app/node/node.h | 3 +- app/node/output.cpp | 4 +- app/node/output.h | 2 +- app/node/output/track/track.cpp | 2 +- app/node/output/track/track.h | 2 +- app/node/param.h | 13 +- app/project/item/folder/folder.cpp | 4 +- app/project/item/folder/folder.h | 2 +- app/project/item/footage/footage.cpp | 4 +- app/project/item/footage/footage.h | 2 +- app/project/item/item.h | 3 +- app/project/item/sequence/sequence.cpp | 9 +- app/project/item/sequence/sequence.h | 2 +- app/project/project.cpp | 9 +- app/render/backend/audiorenderbackend.cpp | 4 +- app/render/backend/videorenderbackend.cpp | 5 +- app/widget/CMakeLists.txt | 1 + app/widget/nodecopypaste/CMakeLists.txt | 22 +++ app/widget/nodecopypaste/nodecopypaste.cpp | 138 ++++++++++++++++ app/widget/nodecopypaste/nodecopypaste.h | 26 +++ app/widget/nodeview/nodeview.cpp | 8 +- app/widget/nodeview/nodeview.h | 3 +- app/widget/timelinewidget/timelinewidget.cpp | 161 +++++++++++++++++-- app/widget/timelinewidget/timelinewidget.h | 17 +- app/widget/timelinewidget/tool/tool.cpp | 51 +----- 35 files changed, 466 insertions(+), 233 deletions(-) create mode 100644 app/widget/nodecopypaste/CMakeLists.txt create mode 100644 app/widget/nodecopypaste/nodecopypaste.cpp create mode 100644 app/widget/nodecopypaste/nodecopypaste.h diff --git a/app/common/xmlutils.cpp b/app/common/xmlutils.cpp index 790abe546..6dcb15dc2 100644 --- a/app/common/xmlutils.cpp +++ b/app/common/xmlutils.cpp @@ -1,16 +1,17 @@ #include "xmlutils.h" #include "node/factory.h" +#include "widget/nodeview/nodeviewundo.h" Node* XMLLoadNode(QXmlStreamReader* reader) { QString node_id; + quintptr node_ptr = 0; XMLAttributeLoop(reader, attr) { - if (attr.name() == "id") { + if (attr.name() == QStringLiteral("id")) { node_id = attr.value().toString(); - - // Currently the only thing we need - break; + } else if (attr.name() == QStringLiteral("ptr")) { + node_ptr = attr.value().toULongLong(); } } @@ -21,20 +22,26 @@ Node* XMLLoadNode(QXmlStreamReader* reader) { Node* node = NodeFactory::CreateFromID(node_id); - if (!node) { + if (node) { + node->setProperty("xml_ptr", node_ptr); + } else { qWarning() << "Failed to load" << node_id << "- no node with that ID is installed"; } return node; } -void XMLConnectNodes(const QHash& output_ptrs, const QList& desired_connections) +void XMLConnectNodes(const QHash& output_ptrs, const QList& desired_connections, QUndoCommand *command) { - foreach (const NodeParam::SerializedConnection& con, desired_connections) { + foreach (const XMLNodeData::SerializedConnection& con, desired_connections) { NodeOutput* out = output_ptrs.value(con.output); if (out) { - NodeParam::ConnectEdge(out, con.input); + if (command) { + new NodeEdgeAddCommand(out, con.input, command); + } else { + NodeParam::ConnectEdge(out, con.input); + } } } } diff --git a/app/common/xmlutils.h b/app/common/xmlutils.h index d3530abb3..22b1c76b1 100644 --- a/app/common/xmlutils.h +++ b/app/common/xmlutils.h @@ -1,9 +1,15 @@ #ifndef XMLREADLOOP_H #define XMLREADLOOP_H +#include #include -#include "node/node.h" +class Node; +class NodeParam; +class NodeInput; +class NodeOutput; + +#include "project/item/footage/stream.h" #define XMLAttributeLoop(reader, item) \ QXmlStreamAttributes __attributes = reader->attributes(); \ @@ -11,7 +17,24 @@ Node *XMLLoadNode(QXmlStreamReader* reader); -void XMLConnectNodes(const QHash &output_ptrs, const QList &desired_connections); +struct XMLNodeData { + struct SerializedConnection { + NodeInput* input; + quintptr output; + }; + + struct FootageConnection { + NodeInput* input; + quintptr footage; + }; + + QHash output_ptrs; + QList desired_connections; + QHash footage_ptrs; + QList footage_connections; +}; + +void XMLConnectNodes(const QHash &output_ptrs, const QList &desired_connections, QUndoCommand* command = nullptr); bool XMLReadNextStartElement(QXmlStreamReader* reader); diff --git a/app/core.cpp b/app/core.cpp index b4755c3f7..f7cfca5c5 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -513,103 +513,14 @@ void Core::SetAutorecoveryInterval(int minutes) autorecovery_timer_.setInterval(minutes * 60000); } -void Core::CopyNodesToClipboard(const QList &nodes) +void Core::CopyStringToClipboard(const QString &s) { - QString copy_str; - - QXmlStreamWriter writer(©_str); - writer.setAutoFormatting(true); - - writer.writeStartDocument(); - writer.writeStartElement(QStringLiteral("olive")); - - foreach (Node* n, nodes) { - n->Save(&writer); - } - - writer.writeEndElement(); // clipboard - writer.writeEndDocument(); - - QGuiApplication::clipboard()->setText(copy_str); + QGuiApplication::clipboard()->setText(s); } -QList Core::PasteNodesFromClipboard(Sequence *graph) +QString Core::PasteStringFromClipboard() { - QString clipboard = QGuiApplication::clipboard()->text(); - - if (clipboard.isEmpty()) { - return QList(); - } - - QXmlStreamReader reader(clipboard); - - QList pasted_nodes; - QHash output_ptrs; - QList desired_connections; - QList footage_connections; - - while (XMLReadNextStartElement(&reader)) { - if (reader.name() == QStringLiteral("olive")) { - while (XMLReadNextStartElement(&reader)) { - if (reader.name() == QStringLiteral("node")) { - Node* node = XMLLoadNode(&reader); - - if (node) { - node->Load(&reader, output_ptrs, desired_connections, footage_connections, nullptr); - - graph->AddNode(node); - - pasted_nodes.append(node); - } - } else { - reader.skipCurrentElement(); - } - } - } else { - reader.skipCurrentElement(); - } - } - - // Make connections - if (!desired_connections.isEmpty()) { - XMLConnectNodes(output_ptrs, desired_connections); - } - - // Connect footage to existing footage if it exists - if (!footage_connections.isEmpty()) { - // Get list of all footage from project - // FIXME: Assumes sequence - QList footage = graph->project()->get_items_of_type(Item::kFootage); - - if (!footage.isEmpty()) { - foreach (const NodeInput::FootageConnection& con, footage_connections) { - if (con.footage) { - // Assume this is a pointer to a Stream* - Stream* loaded_stream = reinterpret_cast(con.footage); - - bool found = false; - - foreach (ItemPtr item, footage) { - const QList& streams = std::static_pointer_cast(item)->streams(); - - foreach (StreamPtr s, streams) { - if (s.get() == loaded_stream) { - con.input->set_standard_value(QVariant::fromValue(s)); - found = true; - break; - } - } - - if (found) { - break; - } - } - } - } - } - } - - return pasted_nodes; + return QGuiApplication::clipboard()->text(); } bool Core::SaveActiveProject() diff --git a/app/core.h b/app/core.h index 702bc10ce..2ebcdf001 100644 --- a/app/core.h +++ b/app/core.h @@ -151,9 +151,9 @@ public: */ void SetAutorecoveryInterval(int minutes); - void CopyNodesToClipboard(const QList& nodes); + static void CopyStringToClipboard(const QString& s); - QList PasteNodesFromClipboard(Sequence *graph); + static QString PasteStringFromClipboard(); /** * @brief Return a list of supported frame rates in rational form diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index a80c69bab..37c0f219d 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -22,6 +22,7 @@ #include +#include "node/output/track/track.h" #include "transition/transition.h" Block::Block() : diff --git a/app/node/input.cpp b/app/node/input.cpp index 8602ab06e..434a2e222 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -20,6 +20,7 @@ #include "input.h" +#include #include #include #include @@ -84,7 +85,7 @@ QString NodeInput::name() return NodeParam::name(); } -void NodeInput::Load(QXmlStreamReader *reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt *cancelled) +void NodeInput::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const QAtomicInt *cancelled) { XMLAttributeLoop(reader, attr) { if (cancelled && *cancelled) { @@ -116,7 +117,7 @@ void NodeInput::Load(QXmlStreamReader *reader, QHash& par if (value_text.isEmpty()) { standard_value_.replace(val_index, QVariant()); } else { - standard_value_.replace(val_index, StringToValue(value_text, footage_connections)); + standard_value_.replace(val_index, StringToValue(value_text, xml_node_data.footage_connections)); } val_index++; @@ -165,7 +166,7 @@ void NodeInput::Load(QXmlStreamReader *reader, QHash& par } } - key_value = StringToValue(reader->readElementText(), footage_connections); + key_value = StringToValue(reader->readElementText(), xml_node_data.footage_connections); NodeKeyframePtr key = NodeKeyframe::Create(key_time, key_value, key_type, track); key->set_bezier_control_in(key_in_handle); @@ -189,13 +190,13 @@ void NodeInput::Load(QXmlStreamReader *reader, QHash& par } if (reader->name() == QStringLiteral("connection")) { - input_connections.append({this, reader->readElementText().toULongLong()}); + xml_node_data.desired_connections.append({this, reader->readElementText().toULongLong()}); } else { reader->skipCurrentElement(); } } } else { - LoadInternal(reader, param_ptrs, input_connections, footage_connections, cancelled); + LoadInternal(reader, xml_node_data, cancelled); } } } @@ -268,7 +269,7 @@ const NodeParam::DataType &NodeInput::data_type() const return data_type_; } -void NodeInput::LoadInternal(QXmlStreamReader* reader, QHash&, QList&, QList&, const QAtomicInt*) +void NodeInput::LoadInternal(QXmlStreamReader* reader, XMLNodeData &, const QAtomicInt*) { reader->skipCurrentElement(); } @@ -289,12 +290,21 @@ QString NodeInput::ValueToString(const QVariant &value) const return value.toString(); } - qWarning() << "Failed to convert type" << data_type_ << "to string"; + if (!value.isNull()) { + qWarning() << "Failed to convert type" << QStringLiteral("%1").arg(data_type_, 0, 16) << "to string"; + } + + /* fall through */ + + // These data types need no XML representation + case kTexture: + case kSamples: + case kBuffer: return QString(); } } -QVariant NodeInput::StringToValue(const QString &string, QList& footage_connections) +QVariant NodeInput::StringToValue(const QString &string, QList& footage_connections) { switch (data_type_) { case kRational: diff --git a/app/node/input.h b/app/node/input.h index 7f75f7190..d29ac92a3 100644 --- a/app/node/input.h +++ b/app/node/input.h @@ -54,7 +54,7 @@ public: virtual QString name() override; - virtual void Load(QXmlStreamReader* reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt* cancelled) override; + virtual void Load(QXmlStreamReader* reader, XMLNodeData& xml_node_data, const QAtomicInt* cancelled) override; virtual void Save(QXmlStreamWriter* writer) const override; @@ -270,14 +270,14 @@ signals: void PropertyChanged(const QString& s, const QVariant& v); protected: - virtual void LoadInternal(QXmlStreamReader* reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt* cancelled); + virtual void LoadInternal(QXmlStreamReader* reader, XMLNodeData& xml_node_data, const QAtomicInt* cancelled); virtual void SaveInternal(QXmlStreamWriter* writer) const; private: QString ValueToString(const QVariant& value) const; - QVariant StringToValue(const QString &string, QList &footage_connections); + QVariant StringToValue(const QString &string, QList &footage_connections); void SaveConnections(QXmlStreamWriter* writer) const; diff --git a/app/node/inputarray.cpp b/app/node/inputarray.cpp index 7fe1fc668..00fd6096d 100644 --- a/app/node/inputarray.cpp +++ b/app/node/inputarray.cpp @@ -164,19 +164,19 @@ void NodeInputArray::RemoveAt(int index) RemoveLast(); } -void NodeInputArray::LoadInternal(QXmlStreamReader *reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt* cancelled) +void NodeInputArray::LoadInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const QAtomicInt* cancelled) { if (reader->name() == QStringLiteral("subparameters")) { while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("input")) { Append(); - At(GetSize() - 1)->Load(reader, param_ptrs, input_connections, footage_connections, cancelled); + At(GetSize() - 1)->Load(reader, xml_node_data, cancelled); } else { reader->skipCurrentElement(); } } } else { - NodeInput::Load(reader, param_ptrs, input_connections, footage_connections, cancelled); + NodeInput::Load(reader, xml_node_data, cancelled); } } diff --git a/app/node/inputarray.h b/app/node/inputarray.h index a0a36d6c1..ceaa6381d 100644 --- a/app/node/inputarray.h +++ b/app/node/inputarray.h @@ -33,7 +33,7 @@ signals: void SizeChanged(int size); protected: - virtual void LoadInternal(QXmlStreamReader* reader, QHash ¶m_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt *cancelled) override; + virtual void LoadInternal(QXmlStreamReader* reader, XMLNodeData& xml_node_data, const QAtomicInt *cancelled) override; virtual void SaveInternal(QXmlStreamWriter* writer) const override; diff --git a/app/node/node.cpp b/app/node/node.cpp index 9d8d048de..a8313980d 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -50,7 +50,7 @@ Node::~Node() } } -void Node::Load(QXmlStreamReader *reader, QHash &output_ptrs, QList& input_connections, QList& footage_connections, const QAtomicInt* cancelled) +void Node::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const QAtomicInt* cancelled) { while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { @@ -80,7 +80,7 @@ void Node::Load(QXmlStreamReader *reader, QHash &output_ continue; } - param->Load(reader, output_ptrs, input_connections, footage_connections, cancelled); + param->Load(reader, xml_node_data, cancelled); } else { reader->skipCurrentElement(); } @@ -89,9 +89,11 @@ void Node::Load(QXmlStreamReader *reader, QHash &output_ void Node::Save(QXmlStreamWriter *writer, const QString &custom_name) const { - writer->writeStartElement(custom_name.isEmpty() ? "node" : custom_name); + writer->writeStartElement(custom_name.isEmpty() ? QStringLiteral("node") : custom_name); - writer->writeAttribute("id", id()); + writer->writeAttribute(QStringLiteral("id"), id()); + + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); foreach (NodeParam* param, parameters()) { param->Save(writer); diff --git a/app/node/node.h b/app/node/node.h index c698ac2d3..fa20b9450 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -27,6 +27,7 @@ #include #include "common/rational.h" +#include "common/xmlutils.h" #include "node/dependency.h" #include "node/input.h" #include "node/inputarray.h" @@ -67,7 +68,7 @@ public: /** * @brief Clear current node variables and replace them with */ - void Load(QXmlStreamReader* reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt *cancelled); + void Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, const QAtomicInt *cancelled); /** * @brief Save this node into a text/XML format diff --git a/app/node/output.cpp b/app/node/output.cpp index 79646932f..122c50851 100644 --- a/app/node/output.cpp +++ b/app/node/output.cpp @@ -42,7 +42,7 @@ QString NodeOutput::name() return NodeParam::name(); } -void NodeOutput::Load(QXmlStreamReader* reader, QHash& param_ptrs, QList&, QList&, const QAtomicInt *cancelled) +void NodeOutput::Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, const QAtomicInt *cancelled) { XMLAttributeLoop(reader, attr) { if (cancelled && *cancelled) { @@ -52,7 +52,7 @@ void NodeOutput::Load(QXmlStreamReader* reader, QHash& pa if (attr.name() == "ptr") { quintptr saved_ptr = attr.value().toULongLong(); - param_ptrs.insert(saved_ptr, this); + xml_node_data.output_ptrs.insert(saved_ptr, this); } } diff --git a/app/node/output.h b/app/node/output.h index 0bbf594e7..1789182f1 100644 --- a/app/node/output.h +++ b/app/node/output.h @@ -42,7 +42,7 @@ public: virtual QString name() override; - virtual void Load(QXmlStreamReader* reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt* cancelled) override; + virtual void Load(QXmlStreamReader* reader, XMLNodeData& xml_node_data, const QAtomicInt* cancelled) override; virtual void Save(QXmlStreamWriter* writer) const override; diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 7c2b4efc7..277a096f4 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -331,7 +331,7 @@ void TrackOutput::ReplaceBlock(Block *old, Block *replace) } } -TrackOutput *TrackOutput::TrackFromBlock(Block *block) +TrackOutput *TrackOutput::TrackFromBlock(const Block *block) { NodeOutput* output = block->output(); diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index f16f0127d..b8dc76d21 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -130,7 +130,7 @@ public: void UnblockInvalidateCache(); - static TrackOutput* TrackFromBlock(Block* block); + static TrackOutput* TrackFromBlock(const Block *block); const rational& track_length() const; diff --git a/app/node/param.h b/app/node/param.h index 849a4a57e..73d488efb 100644 --- a/app/node/param.h +++ b/app/node/param.h @@ -27,6 +27,7 @@ #include #include "common/rational.h" +#include "common/xmlutils.h" #include "node/edge.h" class Node; @@ -222,20 +223,10 @@ public: virtual ~NodeParam() override; - struct SerializedConnection { - NodeInput* input; - quintptr output; - }; - - struct FootageConnection { - NodeInput* input; - quintptr footage; - }; - /** * @brief Load function */ - virtual void Load(QXmlStreamReader* reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt* cancelled) = 0; + virtual void Load(QXmlStreamReader* reader, XMLNodeData& xml_node_data, const QAtomicInt* cancelled) = 0; /** * @brief Save function diff --git a/app/project/item/folder/folder.cpp b/app/project/item/folder/folder.cpp index d19851628..105e9e075 100644 --- a/app/project/item/folder/folder.cpp +++ b/app/project/item/folder/folder.cpp @@ -44,7 +44,7 @@ QIcon Folder::icon() return icon::Folder; } -void Folder::Load(QXmlStreamReader *reader, QHash &footage_ptrs, QList& footage_connections, const QAtomicInt *cancelled) +void Folder::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const QAtomicInt *cancelled) { XMLAttributeLoop(reader, attr) { if (cancelled && *cancelled) { @@ -75,7 +75,7 @@ void Folder::Load(QXmlStreamReader *reader, QHash &footage_ } add_child(child); - child->Load(reader, footage_ptrs, footage_connections, cancelled); + child->Load(reader, xml_node_data, cancelled); } } diff --git a/app/project/item/folder/folder.h b/app/project/item/folder/folder.h index 145f3cd47..d774b1214 100644 --- a/app/project/item/folder/folder.h +++ b/app/project/item/folder/folder.h @@ -42,7 +42,7 @@ public: virtual QIcon icon() override; - virtual void Load(QXmlStreamReader* reader, QHash &footage_ptrs, QList &footage_connections, const QAtomicInt *cancelled) override; + virtual void Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, const QAtomicInt *cancelled) override; virtual void Save(QXmlStreamWriter* writer) const override; diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index 6bd7b9c2a..6e8e7d3dc 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -38,7 +38,7 @@ Footage::~Footage() ClearStreams(); } -void Footage::Load(QXmlStreamReader *reader, QHash& footage_ptrs, QList&, const QAtomicInt* cancelled) +void Footage::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const QAtomicInt* cancelled) { QXmlStreamAttributes attributes = reader->attributes(); @@ -74,7 +74,7 @@ void Footage::Load(QXmlStreamReader *reader, QHash& footage } if (stream_index > -1 && stream_ptr > 0) { - footage_ptrs.insert(stream_ptr, stream(stream_index)); + xml_node_data.footage_ptrs.insert(stream_ptr, stream(stream_index)); stream(stream_index)->Load(reader); } else { diff --git a/app/project/item/footage/footage.h b/app/project/item/footage/footage.h index 27d1206d0..e44bc2142 100644 --- a/app/project/item/footage/footage.h +++ b/app/project/item/footage/footage.h @@ -64,7 +64,7 @@ public: /** * @brief Load function */ - virtual void Load(QXmlStreamReader* reader, QHash &footage_ptrs, QList &footage_connections, const QAtomicInt *cancelled) override; + virtual void Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, const QAtomicInt *cancelled) override; /** * @brief Save function diff --git a/app/project/item/item.h b/app/project/item/item.h index 563e9543c..270fd3be7 100644 --- a/app/project/item/item.h +++ b/app/project/item/item.h @@ -30,6 +30,7 @@ #include "common/constructors.h" #include "common/threadedobject.h" +#include "common/xmlutils.h" #include "node/param.h" #include "project/item/footage/stream.h" @@ -65,7 +66,7 @@ public: DISABLE_COPY_MOVE(Item) - virtual void Load(QXmlStreamReader* reader, QHash &footage_ptrs, QList &footage_connections, const QAtomicInt *cancelled) = 0; + virtual void Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, const QAtomicInt *cancelled) = 0; virtual void Save(QXmlStreamWriter* writer) const = 0; diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index cb681c3b4..05b9f7a1b 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -42,7 +42,7 @@ Sequence::Sequence() AddNode(viewer_output_); } -void Sequence::Load(QXmlStreamReader *reader, QHash &, QList& footage_connections, const QAtomicInt *cancelled) +void Sequence::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const QAtomicInt *cancelled) { XMLAttributeLoop(reader, attr) { if (cancelled && *cancelled) { @@ -56,9 +56,6 @@ void Sequence::Load(QXmlStreamReader *reader, QHash &, QLis } } - QHash output_ptrs; - QList desired_connections; - while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; @@ -110,7 +107,7 @@ void Sequence::Load(QXmlStreamReader *reader, QHash &, QLis } if (node) { - node->Load(reader, output_ptrs, desired_connections, footage_connections, cancelled); + node->Load(reader, xml_node_data, cancelled); AddNode(node); } @@ -120,7 +117,7 @@ void Sequence::Load(QXmlStreamReader *reader, QHash &, QLis } // Make connections - XMLConnectNodes(output_ptrs, desired_connections); + XMLConnectNodes(xml_node_data.output_ptrs, xml_node_data.desired_connections); // Ensure this and all children are in the main thread // (FIXME: Weird place for this? This should probably be in ProjectLoadManager somehow) diff --git a/app/project/item/sequence/sequence.h b/app/project/item/sequence/sequence.h index 1f5336226..379efbe65 100644 --- a/app/project/item/sequence/sequence.h +++ b/app/project/item/sequence/sequence.h @@ -43,7 +43,7 @@ public: /** * @brief Load function */ - virtual void Load(QXmlStreamReader* reader, QHash &footage_ptrs, QList &footage_connections, const QAtomicInt* cancelled) override; + virtual void Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, const QAtomicInt* cancelled) override; /** * @brief Save function diff --git a/app/project/project.cpp b/app/project/project.cpp index 967fa75ae..38fc3823c 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -35,14 +35,13 @@ Project::Project() void Project::Load(QXmlStreamReader *reader, const QAtomicInt* cancelled) { - QHash footage_ptrs; - QList footage_connections; + XMLNodeData xml_node_data; while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("folder")) { // Assume this folder is our root - root_.Load(reader, footage_ptrs, footage_connections, cancelled); + root_.Load(reader, xml_node_data, cancelled); } else if (reader->name() == QStringLiteral("colormanagement")) { @@ -62,9 +61,9 @@ void Project::Load(QXmlStreamReader *reader, const QAtomicInt* cancelled) } } - foreach (const NodeInput::FootageConnection& con, footage_connections) { + foreach (const XMLNodeData::FootageConnection& con, xml_node_data.footage_connections) { if (con.footage) { - con.input->set_standard_value(QVariant::fromValue(footage_ptrs.value(con.footage))); + con.input->set_standard_value(QVariant::fromValue(xml_node_data.footage_ptrs.value(con.footage))); } } } diff --git a/app/render/backend/audiorenderbackend.cpp b/app/render/backend/audiorenderbackend.cpp index 32348310d..8ad0eff57 100644 --- a/app/render/backend/audiorenderbackend.cpp +++ b/app/render/backend/audiorenderbackend.cpp @@ -16,12 +16,12 @@ AudioRenderBackend::AudioRenderBackend(QObject *parent) : void AudioRenderBackend::SetParameters(const AudioRenderingParams ¶ms) { + CancelQueue(); + // Set new parameters params_ = params; // Set params on all processors - // FIXME: Undefined behavior if the processors are currently working, this may need to be delayed like the - // recompile signal foreach (RenderWorker* worker, processors_) { static_cast(worker)->SetParameters(params_); } diff --git a/app/render/backend/videorenderbackend.cpp b/app/render/backend/videorenderbackend.cpp index 5a6c38a32..d85d66899 100644 --- a/app/render/backend/videorenderbackend.cpp +++ b/app/render/backend/videorenderbackend.cpp @@ -85,10 +85,7 @@ void VideoRenderBackend::SetParameters(const VideoRenderingParams& params) void VideoRenderBackend::SetOperatingMode(const VideoRenderWorker::OperatingMode &mode) { - if (!AllProcessorsAreAvailable()) { - qCritical() << "Attempted to set operating mode on a backend whose workers are still busy"; - return; - } + CancelQueue(); operating_mode_ = mode; diff --git a/app/widget/CMakeLists.txt b/app/widget/CMakeLists.txt index 842d835a5..1832bae26 100644 --- a/app/widget/CMakeLists.txt +++ b/app/widget/CMakeLists.txt @@ -23,6 +23,7 @@ add_subdirectory(focusablelineedit) add_subdirectory(footagecombobox) add_subdirectory(keyframeview) add_subdirectory(menu) +add_subdirectory(nodecopypaste) add_subdirectory(nodeview) add_subdirectory(nodeparamview) add_subdirectory(panel) diff --git a/app/widget/nodecopypaste/CMakeLists.txt b/app/widget/nodecopypaste/CMakeLists.txt new file mode 100644 index 000000000..5d21f27d8 --- /dev/null +++ b/app/widget/nodecopypaste/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/nodecopypaste/nodecopypaste.h + widget/nodecopypaste/nodecopypaste.cpp + PARENT_SCOPE +) diff --git a/app/widget/nodecopypaste/nodecopypaste.cpp b/app/widget/nodecopypaste/nodecopypaste.cpp new file mode 100644 index 000000000..64aa30f70 --- /dev/null +++ b/app/widget/nodecopypaste/nodecopypaste.cpp @@ -0,0 +1,138 @@ +#include "nodecopypaste.h" + +#include + +#include "core.h" +#include "widget/nodeview/nodeviewundo.h" +#include "window/mainwindow/mainwindow.h" + +void NodeCopyPasteWidget::CopyNodesToClipboard(const QList &nodes, void *userdata) +{ + QString copy_str; + + QXmlStreamWriter writer(©_str); + writer.setAutoFormatting(true); + + writer.writeStartDocument(); + writer.writeStartElement(QStringLiteral("olive")); + + foreach (Node* n, nodes) { + n->Save(&writer); + } + + CopyNodesToClipboardInternal(&writer, userdata); + + writer.writeEndElement(); // olive + writer.writeEndDocument(); + + Core::CopyStringToClipboard(copy_str); +} + +QList NodeCopyPasteWidget::PasteNodesFromClipboard(Sequence *graph, QUndoCommand* command, void *userdata) +{ + QString clipboard = Core::PasteStringFromClipboard(); + + if (clipboard.isEmpty()) { + return QList(); + } + + QXmlStreamReader reader(clipboard); + + QList pasted_nodes; + XMLNodeData xml_node_data; + + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("olive")) { + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("node")) { + Node* node = XMLLoadNode(&reader); + + if (node) { + node->Load(&reader, xml_node_data, nullptr); + + pasted_nodes.append(node); + } + } else { + PasteNodesFromClipboardInternal(&reader, userdata); + } + } + } else { + reader.skipCurrentElement(); + } + } + + if (pasted_nodes.isEmpty()) { + // If we passed through the whole string and there were no nodes, it must not be data for us after all + return QList(); + } + + // If we have some nodes AND the XML data was malformed, the user should probably know + if (reader.hasError()) { + // Delete all nodes so this is a no-op + foreach (Node* n, pasted_nodes) { + delete n; + } + + // If this was NOT an internal error, we assume it's an XML error that the user needs to know about + QMessageBox::critical(Core::instance()->main_window(), + QCoreApplication::translate("NodeCopyPasteWidget", "Error pasting nodes"), + QCoreApplication::translate("NodeCopyPasteWidget", "Failed to paste nodes: %1").arg(reader.errorString()), + QMessageBox::Ok); + + return QList(); + } + + // Add all nodes to graph + foreach (Node* n, pasted_nodes) { + new NodeAddCommand(graph, n, command); + } + + // Make connections + if (!xml_node_data.desired_connections.isEmpty()) { + XMLConnectNodes(xml_node_data.output_ptrs, xml_node_data.desired_connections, command); + } + + // Connect footage to existing footage if it exists + if (!xml_node_data.footage_connections.isEmpty()) { + // Get list of all footage from project + QList footage = graph->project()->get_items_of_type(Item::kFootage); + + if (!footage.isEmpty()) { + foreach (const XMLNodeData::FootageConnection& con, xml_node_data.footage_connections) { + if (con.footage) { + // Assume this is a pointer to a Stream* + Stream* loaded_stream = reinterpret_cast(con.footage); + + bool found = false; + + foreach (ItemPtr item, footage) { + const QList& streams = std::static_pointer_cast(item)->streams(); + + foreach (StreamPtr s, streams) { + if (s.get() == loaded_stream) { + con.input->set_standard_value(QVariant::fromValue(s)); + found = true; + break; + } + } + + if (found) { + break; + } + } + } + } + } + } + + return pasted_nodes; +} + +void NodeCopyPasteWidget::CopyNodesToClipboardInternal(QXmlStreamWriter*, void*) +{ +} + +void NodeCopyPasteWidget::PasteNodesFromClipboardInternal(QXmlStreamReader* reader, void*) +{ + reader->skipCurrentElement(); +} diff --git a/app/widget/nodecopypaste/nodecopypaste.h b/app/widget/nodecopypaste/nodecopypaste.h new file mode 100644 index 000000000..fc2f6485e --- /dev/null +++ b/app/widget/nodecopypaste/nodecopypaste.h @@ -0,0 +1,26 @@ +#ifndef NODECOPYPASTEWIDGET_H +#define NODECOPYPASTEWIDGET_H + +#include +#include + +#include "node/node.h" +#include "project/item/sequence/sequence.h" + +class NodeCopyPasteWidget +{ +public: + NodeCopyPasteWidget() = default; + +protected: + void CopyNodesToClipboard(const QList& nodes, void* userdata = nullptr); + + QList PasteNodesFromClipboard(Sequence *graph, QUndoCommand *command, void* userdata = nullptr); + + virtual void CopyNodesToClipboardInternal(QXmlStreamWriter *writer, void* userdata); + + virtual void PasteNodesFromClipboardInternal(QXmlStreamReader *reader, void* userdata); + +}; + +#endif // NODECOPYPASTEWIDGET_H diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 5702fb6b6..09549cf71 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -143,7 +143,7 @@ void NodeView::CopySelected(bool cut) return; } - Core::instance()->CopyNodesToClipboard(selected); + CopyNodesToClipboard(selected); if (cut) { DeleteSelected(); @@ -156,7 +156,11 @@ void NodeView::Paste() return; } - QList pasted_nodes = Core::instance()->PasteNodesFromClipboard(static_cast(graph_)); + QUndoCommand* command = new QUndoCommand(); + + QList pasted_nodes = PasteNodesFromClipboard(static_cast(graph_), command); + + Core::instance()->undo_stack()->pushIfHasChildren(command); if (!pasted_nodes.isEmpty()) { // FIXME: Attach to cursor so user can drop in place diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index d566d3be6..8d6a32381 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -26,6 +26,7 @@ #include "node/graph.h" #include "nodeviewscene.h" +#include "widget/nodecopypaste/nodecopypaste.h" /** * @brief A widget for viewing and editing node graphs @@ -33,7 +34,7 @@ * This widget takes a NodeGraph object and constructs a QGraphicsScene representing its data, viewing and allowing * the user to make modifications to it. */ -class NodeView : public QGraphicsView +class NodeView : public QGraphicsView, public NodeCopyPasteWidget { Q_OBJECT public: diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 456f0fe44..0a00e6553 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -135,11 +135,8 @@ void TimelineWidget::Clear() QMap::iterator iterator = block_items_.begin(); while (iterator != block_items_.end()) { - TimelineViewBlockItem* item = iterator.value(); - + delete iterator.value(); iterator = block_items_.erase(iterator); - - delete item; } block_items_.clear(); @@ -254,6 +251,72 @@ void TimelineWidget::DisconnectNodeInternal(ViewerOutput *n) } } +void TimelineWidget::CopyNodesToClipboardInternal(QXmlStreamWriter *writer, void* userdata) +{ + writer->writeStartElement(QStringLiteral("timeline")); + + // Cache the earliest in point so all copied clips have a "relative" in point that can be pasted anywhere + QList& selected = *static_cast*>(userdata); + rational earliest_in = RATIONAL_MAX; + + foreach (TimelineViewBlockItem* item, selected) { + Block* block = item->block(); + + earliest_in = qMin(earliest_in, block->in()); + } + + foreach (TimelineViewBlockItem* item, selected) { + Block* block = item->block(); + + writer->writeStartElement(QStringLiteral("block")); + + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(block))); + writer->writeAttribute(QStringLiteral("in"), (block->in() - earliest_in).toString()); + + TrackOutput* track = TrackOutput::TrackFromBlock(block); + + if (track) { + writer->writeAttribute(QStringLiteral("tracktype"), QString::number(track->track_type())); + writer->writeAttribute(QStringLiteral("trackindex"), QString::number(track->Index())); + } + + writer->writeEndElement(); + } + + writer->writeEndElement(); // timeline +} + +void TimelineWidget::PasteNodesFromClipboardInternal(QXmlStreamReader *reader, void *userdata) +{ + if (reader->name() == QStringLiteral("timeline")) { + QList& paste_data = *static_cast*>(userdata); + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("block")) { + BlockPasteData bpd; + + foreach (QXmlStreamAttribute attr, reader->attributes()) { + if (attr.name() == QStringLiteral("ptr")) { + bpd.ptr = attr.value().toULongLong(); + } else if (attr.name() == QStringLiteral("in")) { + bpd.in = rational::fromString(attr.value().toString()); + } else if (attr.name() == QStringLiteral("tracktype")) { + bpd.track_type = static_cast(attr.value().toInt()); + } else if (attr.name() == QStringLiteral("trackindex")) { + bpd.track_index = attr.value().toInt(); + } + } + + paste_data.append(bpd); + + reader->skipCurrentElement(); + } + } + } else { + NodeCopyPasteWidget::PasteNodesFromClipboardInternal(reader, userdata); + } +} + TimelineWidget::DraggedFootage TimelineWidget::FootageToDraggedFootage(Footage *f) { return DraggedFootage(f, f->get_enabled_stream_flags()); @@ -546,7 +609,7 @@ void TimelineWidget::CopySelected(bool cut) } } - Core::instance()->CopyNodesToClipboard(selected_nodes); + CopyNodesToClipboard(selected_nodes, &selected); if (cut) { DeleteSelected(); @@ -559,18 +622,45 @@ void TimelineWidget::Paste(bool insert) return; } - QList pasted = Core::instance()->PasteNodesFromClipboard(static_cast(GetConnectedNode()->parent())); + QUndoCommand* command = new QUndoCommand(); + + QList paste_data; + QList pasted = PasteNodesFromClipboard(static_cast(GetConnectedNode()->parent()), command, &paste_data); + + rational paste_start = GetTime(); if (insert) { - // FIXME: Implement this - } + rational paste_end = GetTime(); - foreach (Node* n, pasted) { - // See if this block is a node and is a top level node - if (n->IsBlock() && !n->output()->IsConnected()) { + foreach (const BlockPasteData& bpd, paste_data) { + foreach (Node* n, pasted) { + if (n->property("xml_ptr") == bpd.ptr) { + paste_end = qMax(paste_end, paste_start + bpd.in + static_cast(n)->length()); + break; + } + } + } + if (paste_end != paste_start) { + InsertGapsAt(paste_start, paste_end - paste_start, command); } } + + foreach (const BlockPasteData& bpd, paste_data) { + foreach (Node* n, pasted) { + if (n->property("xml_ptr") == bpd.ptr) { + qDebug() << "Placing" << n; + new TrackPlaceBlockCommand(GetConnectedNode()->track_list(bpd.track_type), + bpd.track_index, + static_cast(n), + paste_start + bpd.in, + command); + break; + } + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); } QList TimelineWidget::GetSelectedBlocks() @@ -657,6 +747,55 @@ void TimelineWidget::RippleEditTo(Timeline::MovementMode mode, bool insert_gaps) } } +void TimelineWidget::InsertGapsAt(const rational &earliest_point, const rational &insert_length, QUndoCommand *command) +{ + QVector blocks_to_split; + QList blocks_to_append_gap_to; + QList gaps_to_extend; + + foreach (TrackOutput* track, GetConnectedNode()->Tracks()) { + if (track->IsLocked()) { + continue; + } + + foreach (Block* b, track->Blocks()) { + if (b->out() >= earliest_point) { + if (b->type() == Block::kClip) { + + if (b->out() > earliest_point) { + blocks_to_split.append(b); + } + + blocks_to_append_gap_to.append(b); + + } else if (b->type() == Block::kGap) { + + gaps_to_extend.append(b); + + } + + break; + } + } + } + + // Extend gaps that already exist + foreach (Block* gap, gaps_to_extend) { + new BlockResizeCommand(gap, gap->length() + insert_length, command); + } + + // Split clips here + new BlockSplitPreservingLinksCommand(blocks_to_split, {earliest_point}, command); + + // Insert gaps that don't exist yet + foreach (Block* b, blocks_to_append_gap_to) { + GapBlock* gap = new GapBlock(); + gap->set_length_and_media_out(insert_length); + new NodeAddCommand(static_cast(GetConnectedNode()->parent()), gap, command); + new TrackInsertBlockAfterCommand(TrackOutput::TrackFromBlock(b), gap, b, command); + } +} + TrackOutput *TimelineWidget::GetTrackFromReference(const TrackReference &ref) { return GetConnectedNode()->track_list(ref.type())->TrackAt(ref.index()); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index cca1a6a9e..540d0630d 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -8,6 +8,7 @@ #include "core.h" #include "timelineandtrackview.h" #include "node/output/viewer/viewer.h" +#include "widget/nodecopypaste/nodecopypaste.h" #include "widget/slider/timeslider.h" #include "widget/timebased/timebased.h" @@ -16,7 +17,7 @@ * * Encapsulates TimelineViews, TimeRulers, and scrollbars for a complete widget to manipulate Timelines */ -class TimelineWidget : public TimeBasedWidget +class TimelineWidget : public TimeBasedWidget, public NodeCopyPasteWidget { Q_OBJECT public: @@ -78,6 +79,16 @@ protected: virtual void ConnectNodeInternal(ViewerOutput* n) override; virtual void DisconnectNodeInternal(ViewerOutput* n) override; + virtual void CopyNodesToClipboardInternal(QXmlStreamWriter *writer, void* userdata) override; + virtual void PasteNodesFromClipboardInternal(QXmlStreamReader *reader, void* userdata) override; + + struct BlockPasteData { + quintptr ptr; + rational in; + Timeline::TrackType track_type; + int track_index; + }; + private: class DraggedFootage { public: @@ -162,8 +173,6 @@ private: */ bool SnapPoint(QList start_times, rational *movement, int snap_points = kSnapAll); - void InsertGapsAt(const rational& time, const rational& length, QUndoCommand* command); - void GetGhostData(const QVector& ghosts, rational *earliest_point, rational *latest_point); void InsertGapsAtGhostDestination(const QVector& ghosts, QUndoCommand* command); @@ -382,6 +391,8 @@ private: bool dual_transition_; }; + void InsertGapsAt(const rational& time, const rational& length, QUndoCommand* command); + void DeleteSelectedInternal(const QList& blocks, bool transition_aware, bool remove_from_graph, QUndoCommand* command); void SetBlockLinksSelected(Block *block, bool selected); diff --git a/app/widget/timelinewidget/tool/tool.cpp b/app/widget/timelinewidget/tool/tool.cpp index 6cbe4c33c..1084d5ec8 100644 --- a/app/widget/timelinewidget/tool/tool.cpp +++ b/app/widget/timelinewidget/tool/tool.cpp @@ -197,55 +197,6 @@ bool TimelineWidget::Tool::SnapPoint(QList start_times, rational* move return (diff < DBL_MAX); } -void TimelineWidget::Tool::InsertGapsAt(const rational &earliest_point, const rational &insert_length, QUndoCommand *command) -{ - QVector blocks_to_split; - QList blocks_to_append_gap_to; - QList gaps_to_extend; - - foreach (TrackOutput* track, parent()->GetConnectedNode()->Tracks()) { - if (track->IsLocked()) { - continue; - } - - foreach (Block* b, track->Blocks()) { - if (b->out() >= earliest_point) { - if (b->type() == Block::kClip) { - - if (b->out() > earliest_point) { - blocks_to_split.append(b); - } - - blocks_to_append_gap_to.append(b); - - } else if (b->type() == Block::kGap) { - - gaps_to_extend.append(b); - - } - - break; - } - } - } - - // Extend gaps that already exist - foreach (Block* gap, gaps_to_extend) { - new BlockResizeCommand(gap, gap->length() + insert_length, command); - } - - // Split clips here - new BlockSplitPreservingLinksCommand(blocks_to_split, {earliest_point}, command); - - // Insert gaps that don't exist yet - foreach (Block* b, blocks_to_append_gap_to) { - GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(insert_length); - new NodeAddCommand(static_cast(parent()->GetConnectedNode()->parent()), gap, command); - new TrackInsertBlockAfterCommand(TrackOutput::TrackFromBlock(b), gap, b, command); - } -} - void TimelineWidget::Tool::GetGhostData(const QVector &ghosts, rational *earliest_point, rational *latest_point) { rational ep = RATIONAL_MAX; @@ -271,5 +222,5 @@ void TimelineWidget::Tool::InsertGapsAtGhostDestination(const QVectorInsertGapsAt(earliest_point, latest_point - earliest_point, command); }