From db8d321fa713ad2c5979df637a3f9990363539e7 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 19 Oct 2020 14:37:37 +1100 Subject: [PATCH] project: improved loading and footage importing --- app/codec/ffmpeg/ffmpegdecoder.cpp | 2 +- app/common/xmlutils.cpp | 52 +------- app/common/xmlutils.h | 6 +- app/core.cpp | 62 +++++++++- app/core.h | 5 + app/dialog/CMakeLists.txt | 1 + app/dialog/footagerelink/CMakeLists.txt | 22 ++++ .../footagerelink/footagerelinkdialog.cpp | 103 +++++++++++++++ .../footagerelink/footagerelinkdialog.h | 49 ++++++++ app/node/block/block.cpp | 10 +- app/node/input.cpp | 12 +- app/node/inputarray.cpp | 24 ++-- app/node/node.cpp | 57 ++++++--- app/node/node.h | 2 +- app/node/output.cpp | 4 - app/node/output/track/track.cpp | 12 +- app/node/traverser.cpp | 20 ++- app/project/item/folder/folder.cpp | 20 ++- app/project/item/footage/audiostream.cpp | 24 ++++ app/project/item/footage/audiostream.h | 5 + app/project/item/footage/footage.cpp | 117 ++++++++---------- app/project/item/footage/footage.h | 13 +- app/project/item/footage/stream.cpp | 61 ++++++++- app/project/item/footage/stream.h | 9 +- app/project/item/footage/videostream.cpp | 44 ++++--- app/project/item/footage/videostream.h | 5 - app/project/item/sequence/sequence.cpp | 27 ++-- app/project/project.cpp | 27 ++-- app/task/project/import/import.cpp | 3 +- app/task/project/load/load.cpp | 18 ++- app/task/project/load/load.h | 22 +++- app/task/project/save/save.cpp | 6 + app/timeline/timelinemarker.cpp | 4 - app/timeline/timelinepoints.cpp | 12 +- app/timeline/timelineworkarea.cpp | 4 - app/widget/nodecopypaste/nodecopypaste.cpp | 21 +++- app/widget/nodecopypaste/nodecopypaste.h | 2 +- app/widget/timelinewidget/timelinewidget.cpp | 68 ++++------ app/widget/timelinewidget/timelinewidget.h | 4 +- 39 files changed, 630 insertions(+), 329 deletions(-) create mode 100644 app/dialog/footagerelink/CMakeLists.txt create mode 100644 app/dialog/footagerelink/footagerelinkdialog.cpp create mode 100644 app/dialog/footagerelink/footagerelinkdialog.h diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index d71b9455c..7a388cd8e 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -811,7 +811,7 @@ uint64_t FFmpegDecoder::ValidateChannelLayout(AVStream* stream) bool FFmpegDecoder::StreamUsesMultipleInstances(StreamPtr stream) { return stream->type() == Stream::kVideo - && !std::static_pointer_cast(stream)->is_image_sequence(); + && std::static_pointer_cast(stream)->video_type() != VideoStream::kVideoTypeStill; } FramePtr FFmpegDecoder::BuffersToNativeFrame(int divider, int width, int height, const rational& ts, uint8_t** input_data, int* input_linesize) diff --git a/app/common/xmlutils.cpp b/app/common/xmlutils.cpp index 9bea8f3b0..ed5f8f735 100644 --- a/app/common/xmlutils.cpp +++ b/app/common/xmlutils.cpp @@ -26,49 +26,6 @@ OLIVE_NAMESPACE_ENTER -Node* XMLLoadNode(QXmlStreamReader* reader) -{ - QString node_id; - quintptr node_ptr = 0; - QPointF node_pos; - QString node_label; - - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("id")) { - node_id = attr.value().toString(); - } else if (attr.name() == QStringLiteral("ptr")) { - node_ptr = attr.value().toULongLong(); - } else if (attr.name() == QStringLiteral("pos")) { - QStringList pos = attr.value().toString().split(':'); - - // Protection in case this file has been messed with - if (pos.size() == 2) { - node_pos.setX(pos.at(0).toDouble()); - node_pos.setY(pos.at(1).toDouble()); - } - } else if (attr.name() == QStringLiteral("label")) { - node_label = attr.value().toString(); - } - } - - if (node_id.isEmpty()) { - qWarning() << "Found node with no ID"; - return nullptr; - } - - Node* node = NodeFactory::CreateFromID(node_id); - - if (node) { - node->setProperty("xml_ptr", node_ptr); - node->SetPosition(node_pos); - node->SetLabel(node_label); - } else { - qWarning() << "Failed to load" << node_id << "- no node with that ID is installed"; - } - - return node; -} - void XMLConnectNodes(const XMLNodeData &xml_node_data, QUndoCommand *command) { foreach (const XMLNodeData::SerializedConnection& con, xml_node_data.desired_connections) { @@ -102,13 +59,8 @@ bool XMLReadNextStartElement(QXmlStreamReader *reader) void XMLLinkBlocks(const XMLNodeData &xml_node_data) { - foreach (const XMLNodeData::BlockLink& l1, xml_node_data.block_links) { - foreach (const XMLNodeData::BlockLink& l2, xml_node_data.block_links) { - if (l1.link == l2.block->property("xml_ptr")) { - Block::Link(l1.block, l2.block); - break; - } - } + foreach (const XMLNodeData::BlockLink& l, xml_node_data.block_links) { + Block::Link(l.block, static_cast(xml_node_data.node_ptrs.value(l.link))); } } diff --git a/app/common/xmlutils.h b/app/common/xmlutils.h index ec406dbca..334508111 100644 --- a/app/common/xmlutils.h +++ b/app/common/xmlutils.h @@ -39,8 +39,6 @@ class Item; QXmlStreamAttributes __attributes = reader->attributes(); \ foreach (const QXmlStreamAttribute& item, __attributes) -Node *XMLLoadNode(QXmlStreamReader* reader); - struct XMLNodeData { struct SerializedConnection { NodeInput* input; @@ -57,6 +55,7 @@ struct XMLNodeData { quintptr link; }; + QHash node_ptrs; QHash output_ptrs; QList desired_connections; QHash footage_ptrs; @@ -64,9 +63,6 @@ struct XMLNodeData { QList block_links; QHash item_ptrs; - QString real_project_url; - QString saved_project_url; - }; void XMLConnectNodes(const XMLNodeData& xml_node_data, QUndoCommand* command = nullptr); diff --git a/app/core.cpp b/app/core.cpp index c4aad1c36..64cea253a 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -40,6 +40,7 @@ #include "config/config.h" #include "dialog/about/about.h" #include "dialog/export/export.h" +#include "dialog/footagerelink/footagerelinkdialog.h" #include "dialog/sequence/sequence.h" #include "dialog/task/task.h" #include "dialog/preferences/preferences.h" @@ -482,12 +483,14 @@ void Core::AddOpenProject(ProjectPtr p) void Core::AddOpenProjectFromTask(Task *task) { - QList projects = static_cast(task)->GetLoadedProjects(); - QList layouts = static_cast(task)->GetLoadedLayouts(); + ProjectLoadTask* load_task = static_cast(task); - for (int i=0; iLoadLayout(layouts.at(i)); + ProjectPtr project = load_task->GetLoadedProject(); + MainWindowLayoutInfo layout = load_task->GetLoadedLayout(); + + if (ValidateFootageInLoadedProject(project, load_task->GetFilenameProjectWasSavedAs())) { + AddOpenProject(project); + main_window_->LoadLayout(layout); } } @@ -560,7 +563,7 @@ bool Core::StartHeadlessExport() CLITaskDialog task_dialog(&plm); if (task_dialog.Run()) { - ProjectPtr p = plm.GetLoadedProjects().first(); + ProjectPtr p = plm.GetLoadedProject(); QList items = p->get_items_of_type(Item::kSequence); // Check if this project contains sequences @@ -1222,6 +1225,53 @@ void Core::CacheActiveSequence(bool in_out_only) } } +bool Core::ValidateFootageInLoadedProject(ProjectPtr project, const QString& project_saved_url) +{ + QList footage_we_couldnt_validate; + + QList project_footage = project->get_items_of_type(Item::kFootage); + + foreach (ItemPtr item, project_footage) { + FootagePtr footage = std::static_pointer_cast(item); + + if (!QFileInfo::exists(footage->filename())) { + // If the footage doesn't exist, it might have moved with the project + const QString& project_current_url = project->filename(); + + if (project_current_url != project_saved_url) { + // Project has definitely moved, try to resolve relative paths + QDir saved_dir(QFileInfo(project_saved_url).dir()); + QDir true_dir(QFileInfo(project_current_url).dir()); + + QString relative_filename = saved_dir.relativeFilePath(footage->filename()); + QString transformed_abs_filename = true_dir.filePath(relative_filename); + + if (QFileInfo::exists(transformed_abs_filename)) { + // Use this file instead + qInfo() << "Resolved" << footage->filename() << "relatively to" << transformed_abs_filename; + footage->set_filename(transformed_abs_filename); + } + } + } + + // Heuristically compare footage to file + if (Footage::CompareFootageToItsFilename(footage)) { + footage->SetValid(); + } else { + footage_we_couldnt_validate.append(footage); + } + } + + if (!footage_we_couldnt_validate.isEmpty()) { + FootageRelinkDialog frd(footage_we_couldnt_validate, main_window_); + if (frd.exec() == QDialog::Rejected) { + return false; + } + } + + return true; +} + bool Core::CloseAllProjects() { return CloseAllProjects(true); diff --git a/app/core.h b/app/core.h index 773dd54e1..edf176a5d 100644 --- a/app/core.h +++ b/app/core.h @@ -268,6 +268,11 @@ public: */ void CacheActiveSequence(bool in_out_only); + /** + * @brief Check each footage object for whether it still exists or has changed + */ + bool ValidateFootageInLoadedProject(ProjectPtr project, const QString &project_saved_url); + public slots: /** * @brief Starts an open file dialog to load a project from file diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt index e289fa48e..61e5eada9 100644 --- a/app/dialog/CMakeLists.txt +++ b/app/dialog/CMakeLists.txt @@ -20,6 +20,7 @@ add_subdirectory(color) add_subdirectory(diskcache) add_subdirectory(export) add_subdirectory(footageproperties) +add_subdirectory(footagerelink) add_subdirectory(keyframeproperties) add_subdirectory(preferences) add_subdirectory(progress) diff --git a/app/dialog/footagerelink/CMakeLists.txt b/app/dialog/footagerelink/CMakeLists.txt new file mode 100644 index 000000000..3d420d890 --- /dev/null +++ b/app/dialog/footagerelink/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} + dialog/footagerelink/footagerelinkdialog.h + dialog/footagerelink/footagerelinkdialog.cpp + PARENT_SCOPE +) diff --git a/app/dialog/footagerelink/footagerelinkdialog.cpp b/app/dialog/footagerelink/footagerelinkdialog.cpp new file mode 100644 index 000000000..eae68b38e --- /dev/null +++ b/app/dialog/footagerelink/footagerelinkdialog.cpp @@ -0,0 +1,103 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "footagerelinkdialog.h" + +#include +#include +#include +#include +#include +#include + +OLIVE_NAMESPACE_ENTER + +FootageRelinkDialog::FootageRelinkDialog(const QList& footage, QWidget* parent) : + QDialog(parent), + footage_(footage) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + + layout->addWidget(new QLabel("The following files couldn't be found. Clips using them will be " + "unplayable until they're relinked.")); + + table_ = new QTreeWidget(); + + table_->setColumnCount(3); + table_->setHeaderLabels({tr("Footage"), tr("Filename"), tr("Actions")}); + table_->setRootIsDecorated(false); + + for (int i=0; isetProperty("index", i); + connect(item_browse_btn, &QPushButton::clicked, this, &FootageRelinkDialog::BrowseForFootage); + item_actions_layout->addWidget(item_browse_btn); + + item->setIcon(0, f->icon()); + item->setText(0, f->name()); + item->setText(1, f->filename()); + + table_->addTopLevelItem(item); + + table_->setItemWidget(item, 2, item_actions); + } + + layout->addWidget(table_); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &FootageRelinkDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &FootageRelinkDialog::reject); + layout->addWidget(buttons); + + setWindowTitle(tr("Relink Footage")); +} + +void FootageRelinkDialog::BrowseForFootage() +{ + int index = sender()->property("index").toInt(); + FootagePtr f = footage_.at(index); + + QFileInfo info(f->filename()); + + QString new_fn = QFileDialog::getOpenFileName(this, + tr("Relink \"%1\"").arg(f->name()), + info.absolutePath(), + QStringLiteral("%1;;%2 (**)").arg(info.fileName(), tr("All Files"))); + + if (!new_fn.isEmpty()) { + f->set_filename(new_fn); + + if (Footage::CompareFootageToItsFilename(f)) { + // Set footage to valid and update icon + f->SetValid(); + + QTreeWidgetItem* item = table_->topLevelItem(index); + item->setIcon(0, f->icon()); + item->setText(1, f->filename()); + } + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/footagerelink/footagerelinkdialog.h b/app/dialog/footagerelink/footagerelinkdialog.h new file mode 100644 index 000000000..1c9984f9c --- /dev/null +++ b/app/dialog/footagerelink/footagerelinkdialog.h @@ -0,0 +1,49 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef FOOTAGERELINKDIALOG_H +#define FOOTAGERELINKDIALOG_H + +#include +#include + +#include "project/item/footage/footage.h" + +OLIVE_NAMESPACE_ENTER + +class FootageRelinkDialog : public QDialog +{ + Q_OBJECT +public: + FootageRelinkDialog(const QList& footage, QWidget* parent = nullptr); + +private: + QTreeWidget* table_; + + QList footage_; + +private slots: + void BrowseForFootage(); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // FOOTAGERELINKDIALOG_H diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 85df9aa3b..68dac495a 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -220,10 +220,12 @@ rational Block::MediaToSequenceTime(const rational &media_time) const void Block::LoadInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data) { - if (reader->name() == QStringLiteral("link")) { - xml_node_data.block_links.append({this, reader->readElementText().toULongLong()}); - } else { - Node::LoadInternal(reader, xml_node_data); + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("link")) { + xml_node_data.block_links.append({this, reader->readElementText().toULongLong()}); + } else { + reader->skipCurrentElement(); + } } } diff --git a/app/node/input.cpp b/app/node/input.cpp index 2c5a9d3eb..c6f8d4475 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -88,7 +88,7 @@ void NodeInput::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const } if (attr.name() == QStringLiteral("keyframing")) { - set_is_keyframing(attr.value() == QStringLiteral("1")); + set_is_keyframing(attr.value().toInt()); } } } @@ -199,16 +199,16 @@ void NodeInput::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const set_property(QStringLiteral("col_view"), reader->readElementText()); } else if (reader->name() == QStringLiteral("cslook")) { set_property(QStringLiteral("col_look"), reader->readElementText()); - } else { + } else if (reader->name() == QStringLiteral("custom")) { LoadInternal(reader, xml_node_data, cancelled); + } else { + reader->skipCurrentElement(); } } } void NodeInput::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement("input"); - writer->writeAttribute("id", id()); writer->writeAttribute("keyframing", QString::number(keyframing_)); @@ -258,9 +258,9 @@ void NodeInput::Save(QXmlStreamWriter *writer) const SaveConnections(writer); + writer->writeStartElement(QStringLiteral("custom")); SaveInternal(writer); - - writer->writeEndElement(); // input + writer->writeEndElement(); // custom } void NodeInput::SaveConnections(QXmlStreamWriter *writer) const diff --git a/app/node/inputarray.cpp b/app/node/inputarray.cpp index b1ec0caa8..d2faf3485 100644 --- a/app/node/inputarray.cpp +++ b/app/node/inputarray.cpp @@ -187,17 +187,19 @@ void NodeInputArray::RemoveAt(int index) 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, xml_node_data, cancelled); - } else { - reader->skipCurrentElement(); + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("subparameters")) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("input")) { + Append(); + At(GetSize() - 1)->Load(reader, xml_node_data, cancelled); + } else { + reader->skipCurrentElement(); + } } + } else { + reader->skipCurrentElement(); } - } else { - NodeInput::Load(reader, xml_node_data, cancelled); } } @@ -206,7 +208,9 @@ void NodeInputArray::SaveInternal(QXmlStreamWriter *writer) const writer->writeStartElement("subparameters"); foreach (NodeInput* sub, sub_params_) { - sub->Save(writer); + writer->writeStartElement(QStringLiteral("input")); + sub->Save(writer); + writer->writeEndElement(); } writer->writeEndElement(); // subparameters diff --git a/app/node/node.cpp b/app/node/node.cpp index 9c5e4a4d5..741ca111e 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -95,34 +95,61 @@ void Node::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const QAto } param->Load(reader, xml_node_data, cancelled); - } else { + } 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("custom")) { LoadInternal(reader, xml_node_data); + } else { + reader->skipCurrentElement(); } } } -void Node::Save(QXmlStreamWriter *writer, const QString &custom_name) const +void Node::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement(custom_name.isEmpty() ? QStringLiteral("node") : custom_name); + writer->writeTextElement(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); - writer->writeAttribute(QStringLiteral("id"), id()); + writer->writeStartElement(QStringLiteral("pos")); + writer->writeTextElement(QStringLiteral("x"), QString::number(GetPosition().x())); + writer->writeTextElement(QStringLiteral("y"), QString::number(GetPosition().y())); + writer->writeEndElement(); // pos - writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); - - writer->writeAttribute(QStringLiteral("pos"), - QStringLiteral("%1:%2").arg(QString::number(GetPosition().x()), - QString::number(GetPosition().y()))); - - writer->writeAttribute(QStringLiteral("label"), - GetLabel()); + writer->writeTextElement(QStringLiteral("label"), GetLabel()); foreach (NodeParam* param, parameters()) { + switch (param->type()) { + case NodeParam::kInput: + writer->writeStartElement(QStringLiteral("input")); + break; + case NodeParam::kOutput: + writer->writeStartElement(QStringLiteral("output")); + break; + } + param->Save(writer); + + writer->writeEndElement(); // input/output } + writer->writeStartElement(QStringLiteral("custom")); SaveInternal(writer); - - writer->writeEndElement(); // node + writer->writeEndElement(); // custom } QString Node::ShortName() const @@ -361,7 +388,7 @@ void Node::Hash(QCryptographicHash &hash, const rational& time) const hash.addData(stream->footage()->filename().toUtf8()); // Footage last modified date - hash.addData(stream->footage()->timestamp().toString().toUtf8()); + hash.addData(QString::number(stream->footage()->timestamp()).toUtf8()); // Footage stream hash.addData(QString::number(stream->index()).toUtf8()); diff --git a/app/node/node.h b/app/node/node.h index 49d3c8738..ef907da37 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -95,7 +95,7 @@ public: /** * @brief Save this node into a text/XML format */ - void Save(QXmlStreamWriter* writer, const QString& custom_name = QString()) const; + void Save(QXmlStreamWriter* writer) const; /** * @brief Return the name of the node diff --git a/app/node/output.cpp b/app/node/output.cpp index 070a64d03..47f5f398d 100644 --- a/app/node/output.cpp +++ b/app/node/output.cpp @@ -63,13 +63,9 @@ void NodeOutput::Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, cons void NodeOutput::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement("output"); - writer->writeAttribute("id", id()); writer->writeAttribute("ptr", QString::number(reinterpret_cast(this))); - - writer->writeEndElement(); // output } OLIVE_NAMESPACE_EXIT diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 92eb93290..e98049e85 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -111,12 +111,14 @@ void TrackOutput::SetTrackHeight(const double &height) emit TrackHeightChangedInPixels(GetTrackHeightInPixels()); } -void TrackOutput::LoadInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data) +void TrackOutput::LoadInternal(QXmlStreamReader *reader, XMLNodeData &) { - if (reader->name() == QStringLiteral("height")) { - SetTrackHeight(reader->readElementText().toDouble()); - } else { - Node::LoadInternal(reader, xml_node_data); + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("height")) { + SetTrackHeight(reader->readElementText().toDouble()); + } else { + reader->skipCurrentElement(); + } } } diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index f8958f28e..1994d8fd5 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -212,10 +212,14 @@ void NodeTraverser::PostProcessTable(const Node *node, const TimeRange &range, N if (!got_cached_frame) { // Retrieve video frames foreach (const NodeValue& v, video_footage_to_retrieve) { - QVariant value = ProcessVideoFootage(v.data().value(), range.in()); + StreamPtr stream = v.data().value(); - if (!value.isNull()) { - output_params.Push(NodeParam::kTexture, value, node); + if (stream->footage()->IsValid()) { + QVariant value = ProcessVideoFootage(stream, range.in()); + + if (!value.isNull()) { + output_params.Push(NodeParam::kTexture, value, node); + } } } @@ -240,10 +244,14 @@ void NodeTraverser::PostProcessTable(const Node *node, const TimeRange &range, N // Retrieve audio samples foreach (const NodeValue& v, audio_footage_to_retrieve) { - QVariant value = ProcessAudioFootage(v.data().value(), range); + StreamPtr stream = v.data().value(); - if (!value.isNull()) { - output_params.Push(NodeParam::kSamples, value, node); + if (stream->footage()->IsValid()) { + QVariant value = ProcessAudioFootage(v.data().value(), range); + + if (!value.isNull()) { + output_params.Push(NodeParam::kSamples, value, node); + } } } diff --git a/app/project/item/folder/folder.cpp b/app/project/item/folder/folder.cpp index d9c727a8b..47e1a60df 100644 --- a/app/project/item/folder/folder.cpp +++ b/app/project/item/folder/folder.cpp @@ -81,17 +81,27 @@ void Folder::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const QA void Folder::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement(QStringLiteral("folder")); - writer->writeAttribute(QStringLiteral("name"), name()); writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); foreach (ItemPtr child, children()) { - child->Save(writer); - } + switch (child->type()) { + case Item::kFootage: + writer->writeStartElement(QStringLiteral("footage")); + break; + case Item::kSequence: + writer->writeStartElement(QStringLiteral("sequence")); + break; + case Item::kFolder: + writer->writeStartElement(QStringLiteral("folder")); + break; + } - writer->writeEndElement(); // folder + child->Save(writer); + + writer->writeEndElement(); // footage/folder/sequence + } } OLIVE_NAMESPACE_EXIT diff --git a/app/project/item/footage/audiostream.cpp b/app/project/item/footage/audiostream.cpp index 7aec20aa5..f824c2ec1 100644 --- a/app/project/item/footage/audiostream.cpp +++ b/app/project/item/footage/audiostream.cpp @@ -20,6 +20,8 @@ #include "audiostream.h" +#include "common/xmlutils.h" + OLIVE_NAMESPACE_ENTER AudioStream::AudioStream() @@ -101,4 +103,26 @@ QIcon AudioStream::icon() const return icon::Audio; } +void AudioStream::LoadCustomParameters(QXmlStreamReader *reader) +{ + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("channels")) { + set_channels(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("layout")) { + set_channel_layout(reader->readElementText().toULongLong()); + } else if (reader->name() == QStringLiteral("rate")) { + set_sample_rate(reader->readElementText().toInt()); + } else { + reader->skipCurrentElement(); + } + } +} + +void AudioStream::SaveCustomParameters(QXmlStreamWriter *writer) const +{ + writer->writeTextElement(QStringLiteral("channels"), QString::number(channels_)); + writer->writeTextElement(QStringLiteral("layout"), QString::number(layout_)); + writer->writeTextElement(QStringLiteral("rate"), QString::number(sample_rate_)); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/project/item/footage/audiostream.h b/app/project/item/footage/audiostream.h index 5bf90072f..f85046839 100644 --- a/app/project/item/footage/audiostream.h +++ b/app/project/item/footage/audiostream.h @@ -55,6 +55,11 @@ public: virtual QIcon icon() const override; +protected: + virtual void LoadCustomParameters(QXmlStreamReader *reader) override; + + virtual void SaveCustomParameters(QXmlStreamWriter* writer) const override; + signals: void ConformAppended(OLIVE_NAMESPACE::AudioParams params); diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index e9e83d706..9e41e72ec 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -24,6 +24,7 @@ #include #include "codec/decoder.h" +#include "common/filefunctions.h" #include "common/xmlutils.h" #include "config/config.h" #include "core.h" @@ -43,91 +44,45 @@ Footage::~Footage() void Footage::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const QAtomicInt* cancelled) { - /* - QXmlStreamAttributes attributes = reader->attributes(); - - foreach (const QXmlStreamAttribute& attr, attributes) { - if (attr.name() == QStringLiteral("name")) { - set_name(attr.value().toString()); - } else if (attr.name() == QStringLiteral("filename")) { - set_filename(attr.value().toString()); - } - } - - // Validate filename - if (!QFileInfo::exists(filename_)) { - // Absolute filename does not exist, use some heuristics to try relocating the file - - if (xml_node_data.real_project_url != xml_node_data.saved_project_url) { - // Project path has changed, check if the file we're looking for is the same relative to the - // new project path - QDir saved_dir(QFileInfo(xml_node_data.saved_project_url).dir()); - QDir true_dir(QFileInfo(xml_node_data.real_project_url).dir()); - - QString relative_filename = saved_dir.relativeFilePath(filename_); - QString transformed_abs_filename = true_dir.filePath(relative_filename); - - if (QFileInfo::exists(transformed_abs_filename)) { - // Use this file instead - qInfo() << "Footage" << filename_ << "doesn't exist, using relative file" << transformed_abs_filename; - set_filename(transformed_abs_filename); - } - } - } - - Decoder::ProbeMedia(this, cancelled); - while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->name() == QStringLiteral("stream")) { - int stream_index = -1; - quintptr stream_ptr = 0; - - XMLAttributeLoop(reader, attr) { - if (cancelled && *cancelled) { - return; - } - - if (attr.name() == QStringLiteral("index")) { - stream_index = attr.value().toInt(); - } else if (attr.name() == QStringLiteral("ptr")) { - stream_ptr = attr.value().toULongLong(); - } - } - - if (stream_index > -1 && stream_ptr > 0) { - xml_node_data.footage_ptrs.insert(stream_ptr, stream(stream_index)); - - stream(stream_index)->Load(reader); - } else { - qWarning() << "Invalid stream found in project file"; - } + if (reader->name() == QStringLiteral("name")) { + set_name(reader->readElementText()); + } else if (reader->name() == QStringLiteral("filename")) { + set_filename(reader->readElementText()); + } else if (reader->name() == QStringLiteral("stream")) { + add_stream(Stream::Load(reader, xml_node_data, cancelled)); + } else if (reader->name() == QStringLiteral("timestamp")) { + set_timestamp(reader->readElementText().toLongLong()); + } else if (reader->name() == QStringLiteral("decoder")) { + set_decoder(reader->readElementText()); } else if (reader->name() == QStringLiteral("points")) { TimelinePoints::Load(reader); } else { reader->skipCurrentElement(); } } - */ } void Footage::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement("footage"); + writer->writeTextElement(QStringLiteral("name"), name()); + writer->writeTextElement(QStringLiteral("filename"), filename()); + writer->writeTextElement(QStringLiteral("timestamp"), QString::number(timestamp_)); + writer->writeTextElement(QStringLiteral("decoder"), decoder_); - writer->writeAttribute("name", name()); - writer->writeAttribute("filename", filename()); - - TimelinePoints::Save(writer); + writer->writeStartElement(QStringLiteral("points")); + TimelinePoints::Save(writer); + writer->writeEndElement(); // points foreach (StreamPtr stream, streams_) { - stream->Save(writer); + writer->writeStartElement(QStringLiteral("stream")); + stream->Save(writer); + writer->writeEndElement(); // stream } - - writer->writeEndElement(); // footage } void Footage::Clear() @@ -154,12 +109,12 @@ void Footage::set_filename(const QString &s) filename_ = s; } -const QDateTime &Footage::timestamp() const +const qint64 &Footage::timestamp() const { return timestamp_; } -void Footage::set_timestamp(const QDateTime &t) +void Footage::set_timestamp(const qint64 &t) { timestamp_ = t; } @@ -341,6 +296,32 @@ StreamPtr Footage::get_first_stream_of_type(const Stream::Type &type) const return nullptr; } +bool Footage::CompareFootageToItsFilename(FootagePtr footage) +{ + // Heuristic to determine if file has changed + QFileInfo info(footage->filename()); + + if (info.exists()) { + if (info.lastModified().toMSecsSinceEpoch() == footage->timestamp()) { + // Footage has not been modified and is where we expect + return true; + } else { + // Footage may have changed and we'll have to re-probe it. It also may not have, in which + // case nothing needs to change. + ItemPtr item = Decoder::ProbeMedia(footage->filename(), nullptr); + + if (item && item->type() == footage->type()) { + // Item is the same type, that's a good sign. Let's look for any differences. + // FIXME: Implement this + return true; + } + } + } + + // Footage file couldn't be found or resolved to something we didn't expect + return false; +} + void Footage::UpdateTooltip() { if (valid_) { diff --git a/app/project/item/footage/footage.h b/app/project/item/footage/footage.h index 6f0b9735e..3348d35cb 100644 --- a/app/project/item/footage/footage.h +++ b/app/project/item/footage/footage.h @@ -32,6 +32,9 @@ OLIVE_NAMESPACE_ENTER +class Footage; +using FootagePtr = std::shared_ptr; + /** * @brief A reference to an external media file with metadata in a project structure * @@ -109,7 +112,7 @@ public: * The file's last modified timestamp is stored for potential organization in the ProjectExplorer. It can be * retrieved here. */ - const QDateTime& timestamp() const; + const qint64 ×tamp() const; /** * @brief Set the last modified time/date @@ -120,7 +123,7 @@ public: * * New last modified time/date */ - void set_timestamp(const QDateTime& t); + void set_timestamp(const qint64 &t); /** * @brief Add a stream metadata object to this footage @@ -199,6 +202,8 @@ public: StreamPtr get_first_stream_of_type(const Stream::Type& type) const; + static bool CompareFootageToItsFilename(FootagePtr footage); + private: /** * @brief Internal function to delete all Stream children and empty the array @@ -229,7 +234,7 @@ private: /** * @brief Internal timestamp object */ - QDateTime timestamp_; + qint64 timestamp_; /** * @brief Internal streams array @@ -245,8 +250,6 @@ private: }; -using FootagePtr = std::shared_ptr; - OLIVE_NAMESPACE_EXIT #endif // FOOTAGE_H diff --git a/app/project/item/footage/stream.cpp b/app/project/item/footage/stream.cpp index 2a59070ea..8598a87e8 100644 --- a/app/project/item/footage/stream.cpp +++ b/app/project/item/footage/stream.cpp @@ -37,22 +37,71 @@ Stream::~Stream() { } -void Stream::Load(QXmlStreamReader *reader) +StreamPtr Stream::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const QAtomicInt* cancelled) { - LoadCustomParameters(reader); + StreamPtr stream; + + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("type")) { + Stream::Type type = static_cast(attr.value().toInt()); + switch (type) { + case Stream::kVideo: + stream = std::make_shared(); + break; + case Stream::kAudio: + stream = std::make_shared(); + break; + default: + stream = std::make_shared(); + stream->set_type(type); + break; + } + + // This is the only attribute we need + break; + } + } + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("ptr")) { + xml_node_data.footage_ptrs.insert(reader->readElementText().toULongLong(), stream); + } else if (reader->name() == QStringLiteral("index")) { + stream->set_index(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("timebase")) { + stream->set_timebase(rational::fromString(reader->readElementText())); + } else if (reader->name() == QStringLiteral("duration")) { + stream->set_duration(reader->readElementText().toLongLong()); + } else if (reader->name() == QStringLiteral("enabled")) { + stream->set_enabled(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("custom")) { + stream->LoadCustomParameters(reader); + } else { + reader->skipCurrentElement(); + } + } + + return stream; } void Stream::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement("stream"); + writer->writeAttribute(QStringLiteral("type"), QString::number(type_)); - writer->writeAttribute("ptr", QString::number(reinterpret_cast(this))); + writer->writeTextElement(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); - writer->writeAttribute("index", QString::number(index_)); + writer->writeTextElement(QStringLiteral("index"), QString::number(index_)); + + writer->writeTextElement(QStringLiteral("timebase"), timebase_.toString()); + + writer->writeTextElement(QStringLiteral("duration"), QString::number(duration_)); + + writer->writeTextElement(QStringLiteral("enabled"), QString::number(enabled_)); + + writer->writeStartElement(QStringLiteral("custom")); SaveCustomParameters(writer); - writer->writeEndElement(); // stream + writer->writeEndElement(); } QString Stream::description() const diff --git a/app/project/item/footage/stream.h b/app/project/item/footage/stream.h index e5165de53..634bce7b0 100644 --- a/app/project/item/footage/stream.h +++ b/app/project/item/footage/stream.h @@ -33,6 +33,9 @@ OLIVE_NAMESPACE_ENTER class Footage; +class Stream; +using StreamPtr = std::shared_ptr; +struct XMLNodeData; /** * @brief A base class for keeping metadata about a media stream. @@ -64,9 +67,9 @@ public: /** * @brief Required virtual destructor, serves no purpose */ - virtual ~Stream(); + virtual ~Stream() override; - void Load(QXmlStreamReader* reader); + static StreamPtr Load(QXmlStreamReader* reader, XMLNodeData& xml_node_data, const QAtomicInt* cancelled); void Save(QXmlStreamWriter *writer) const; @@ -119,8 +122,6 @@ private: }; -using StreamPtr = std::shared_ptr; - OLIVE_NAMESPACE_EXIT #include diff --git a/app/project/item/footage/videostream.cpp b/app/project/item/footage/videostream.cpp index 65c658eff..98cbfd1fc 100644 --- a/app/project/item/footage/videostream.cpp +++ b/app/project/item/footage/videostream.cpp @@ -35,8 +35,7 @@ VideoStream::VideoStream() : interlacing_(VideoParams::kInterlaceNone), video_type_(VideoStream::kVideoTypeVideo), pixel_aspect_ratio_(1), - start_time_(0), - is_image_sequence_(false) + start_time_(0) { set_type(Stream::kVideo); } @@ -75,16 +74,6 @@ void VideoStream::set_start_time(const int64_t &start_time) emit ParametersChanged(); } -bool VideoStream::is_image_sequence() const -{ - return is_image_sequence_; -} - -void VideoStream::set_image_sequence(bool e) -{ - is_image_sequence_ = e; -} - int64_t VideoStream::get_time_in_timebase_units(const rational &time) const { return Timecode::time_to_timestamp(time, timebase()) + start_time(); @@ -102,8 +91,26 @@ QIcon VideoStream::icon() const void VideoStream::LoadCustomParameters(QXmlStreamReader *reader) { while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("colorspace")) { + if (reader->name() == QStringLiteral("width")) { + set_width(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("height")) { + set_height(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("premultiplied")) { + set_premultiplied_alpha(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("colorspace")) { set_colorspace(reader->readElementText()); + } else if (reader->name() == QStringLiteral("interlacing")) { + set_interlacing(static_cast(reader->readElementText().toInt())); + } else if (reader->name() == QStringLiteral("type")) { + set_video_type(static_cast(reader->readElementText().toInt())); + } else if (reader->name() == QStringLiteral("format")) { + set_format(static_cast(reader->readElementText().toInt())); + } else if (reader->name() == QStringLiteral("pixelaspect")) { + set_pixel_aspect_ratio(rational::fromString(reader->readElementText())); + } else if (reader->name() == QStringLiteral("framerate")) { + set_frame_rate(rational::fromString(reader->readElementText())); + } else if (reader->name() == QStringLiteral("starttime")) { + set_start_time(reader->readElementText().toLongLong()); } else { reader->skipCurrentElement(); } @@ -112,7 +119,16 @@ void VideoStream::LoadCustomParameters(QXmlStreamReader *reader) void VideoStream::SaveCustomParameters(QXmlStreamWriter *writer) const { - writer->writeTextElement("colorspace", colorspace_); + writer->writeTextElement(QStringLiteral("width"), QString::number(width_)); + writer->writeTextElement(QStringLiteral("height"), QString::number(height_)); + writer->writeTextElement(QStringLiteral("premultiplied"), QString::number(premultiplied_alpha_)); + writer->writeTextElement(QStringLiteral("colorspace"), colorspace_); + writer->writeTextElement(QStringLiteral("interlacing"), QString::number(interlacing_)); + writer->writeTextElement(QStringLiteral("type"), QString::number(video_type_)); + writer->writeTextElement(QStringLiteral("format"), QString::number(format_)); + writer->writeTextElement(QStringLiteral("pixelaspect"), pixel_aspect_ratio_.toString()); + writer->writeTextElement(QStringLiteral("framerate"), frame_rate_.toString()); + writer->writeTextElement(QStringLiteral("starttime"), QString::number(start_time_)); } bool VideoStream::premultiplied_alpha() const diff --git a/app/project/item/footage/videostream.h b/app/project/item/footage/videostream.h index b9f878d0a..7fe6ba773 100644 --- a/app/project/item/footage/videostream.h +++ b/app/project/item/footage/videostream.h @@ -132,9 +132,6 @@ public: const int64_t& start_time() const; void set_start_time(const int64_t& start_time); - bool is_image_sequence() const; - void set_image_sequence(bool e); - int64_t get_time_in_timebase_units(const rational& time) const; virtual QIcon icon() const override; @@ -166,8 +163,6 @@ private: int64_t start_time_; - bool is_image_sequence_; - }; using VideoStreamPtr = std::shared_ptr; diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index b90feb863..92eb125a0 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -121,7 +121,14 @@ void Sequence::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const Node* node; if (reader->name() == QStringLiteral("node")) { - node = XMLLoadNode(reader); + node = nullptr; + + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("id")) { + node = NodeFactory::CreateFromID(attr.value().toString()); + break; + } + } } else { node = viewer_output_; } @@ -143,7 +150,7 @@ void Sequence::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const XMLLinkBlocks(xml_node_data); // Ensure this and all children are in the main thread - // (FIXME: Weird place for this? This should probably be in ProjectLoadManager somehow) + // NOTE: It might be good to move the Item system to QObjects so they inherit their thread if (thread() != qApp->thread()) { moveToThread(qApp->thread()); } @@ -151,8 +158,6 @@ void Sequence::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const void Sequence::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement(QStringLiteral("sequence")); - writer->writeAttribute(QStringLiteral("name"), name()); writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); @@ -178,17 +183,23 @@ void Sequence::Save(QXmlStreamWriter *writer) const writer->writeEndElement(); // audio // Write TimelinePoints - TimelinePoints::Save(writer); + writer->writeStartElement(QStringLiteral("points")); + TimelinePoints::Save(writer); + writer->writeEndElement(); // points foreach (Node* node, nodes()) { if (node != viewer_output_) { + writer->writeStartElement(QStringLiteral("node")); + writer->writeAttribute(QStringLiteral("id"), node->id()); node->Save(writer); + writer->writeEndElement(); // node; } } - viewer_output_->Save(writer, QStringLiteral("viewer")); - - writer->writeEndElement(); // sequence + writer->writeStartElement(QStringLiteral("viewer")); + writer->writeAttribute(QStringLiteral("id"), viewer_output_->id()); + viewer_output_->Save(writer); + writer->writeEndElement(); // viewer; } void Sequence::add_default_nodes() diff --git a/app/project/project.cpp b/app/project/project.cpp index 0208ffa37..6cf8342e9 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -47,13 +47,9 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, const { XMLNodeData xml_node_data; - // Set project filename (hacky) - xml_node_data.real_project_url = static_cast(reader->device())->fileName(); - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("folder")) { + if (reader->name() == QStringLiteral("root")) { - // Assume this folder is our root root_.Load(reader, xml_node_data, cancelled); } else if (reader->name() == QStringLiteral("colormanagement")) { @@ -82,11 +78,6 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, const *layout = MainWindowLayoutInfo::fromXml(reader, xml_node_data); - } else if (reader->name() == QStringLiteral("url")) { - - // This should be read in before most other elements - xml_node_data.saved_project_url = reader->readElementText(); - } else { // Skip this @@ -104,27 +95,23 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, const void Project::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement("project"); - - writer->writeTextElement("url", filename_); - - writer->writeTextElement("cachepath", cache_path(false)); + writer->writeTextElement(QStringLiteral("cachepath"), cache_path(false)); + writer->writeStartElement(QStringLiteral("root")); root_.Save(writer); + writer->writeEndElement(); - writer->writeStartElement("colormanagement"); + writer->writeStartElement(QStringLiteral("colormanagement")); - writer->writeTextElement("config", color_manager_.GetConfigFilename()); + writer->writeTextElement(QStringLiteral("config"), color_manager_.GetConfigFilename()); - writer->writeTextElement("default", color_manager_.GetDefaultInputColorSpace()); + writer->writeTextElement(QStringLiteral("default"), color_manager_.GetDefaultInputColorSpace()); writer->writeEndElement(); // colormanagement // Save main window project layout MainWindowLayoutInfo main_window_info = Core::instance()->main_window()->SaveLayout(); main_window_info.toXml(writer); - - writer->writeEndElement(); // project } Folder *Project::root() diff --git a/app/task/project/import/import.cpp b/app/task/project/import/import.cpp index c9ec9e2cf..25e723bb4 100644 --- a/app/task/project/import/import.cpp +++ b/app/task/project/import/import.cpp @@ -124,7 +124,7 @@ void ProjectImportTask::Import(Folder *folder, QFileInfoList import, int &counte FootagePtr footage = std::static_pointer_cast(item); footage->set_filename(file_path); - footage->set_timestamp(file_info.lastModified()); + footage->set_timestamp(file_info.lastModified().toMSecsSinceEpoch()); // See if this footage is an image sequence ValidateImageSequence(footage, import, i); @@ -223,7 +223,6 @@ void ProjectImportTask::ValidateImageSequence(ItemPtr item, QFileInfoList& info_ rational default_timebase = Config::Current()["DefaultSequenceFrameRate"].value(); video_stream->set_timebase(default_timebase); video_stream->set_frame_rate(default_timebase.flipped()); - video_stream->set_image_sequence(true); video_stream->set_start_time(start_index); video_stream->set_duration(end_index - start_index + 1); diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 3d945023a..8e3bf3241 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -46,22 +46,18 @@ bool ProjectLoadTask::Run() while(XMLReadNextStartElement(&reader)) { if (reader.name() == QStringLiteral("version")) { qDebug() << "Project version:" << reader.readElementText(); + } else if (reader.name() == QStringLiteral("url")) { + project_saved_url_ = reader.readElementText(); } else if (reader.name() == QStringLiteral("project")) { - ProjectPtr project = std::make_shared(); + project_ = std::make_shared(); - project->set_filename(filename_); + project_->set_filename(filename_); - MainWindowLayoutInfo layout; - - project->Load(&reader, &layout, &IsCancelled()); + project_->Load(&reader, &layout_info_, &IsCancelled()); // Ensure project is in main thread - project->moveToThread(qApp->thread()); - - if (!IsCancelled()) { - projects_.append(project); - layout_info_.append(layout); - } + project_->moveToThread(qApp->thread()); + break; } else { reader.skipCurrentElement(); } diff --git a/app/task/project/load/load.h b/app/task/project/load/load.h index 398853028..5fa0b0da5 100644 --- a/app/task/project/load/load.h +++ b/app/task/project/load/load.h @@ -33,23 +33,35 @@ class ProjectLoadTask : public Task public: ProjectLoadTask(const QString& filename); - const QList& GetLoadedProjects() const + ProjectPtr GetLoadedProject() const { - return projects_; + return project_; } - const QList& GetLoadedLayouts() const + MainWindowLayoutInfo GetLoadedLayout() const { return layout_info_; } + /** + * @brief Returns the filename the project was saved as, but not necessarily where it is now + * + * May help for resolving relative paths. + */ + const QString& GetFilenameProjectWasSavedAs() const + { + return project_saved_url_; + } + protected: virtual bool Run() override; private: - QList projects_; + ProjectPtr project_; - QList layout_info_; + MainWindowLayoutInfo layout_info_; + + QString project_saved_url_; QString filename_; diff --git a/app/task/project/save/save.cpp b/app/task/project/save/save.cpp index 211a70c2a..22cca573a 100644 --- a/app/task/project/save/save.cpp +++ b/app/task/project/save/save.cpp @@ -51,8 +51,14 @@ bool ProjectSaveTask::Run() writer.writeTextElement("version", "0.2.0"); + writer.writeTextElement("url", project_->filename()); + + writer.writeStartElement(QStringLiteral("project")); + project_->Save(&writer); + writer.writeEndElement(); // project + writer.writeEndElement(); // olive writer.writeEndDocument(); diff --git a/app/timeline/timelinemarker.cpp b/app/timeline/timelinemarker.cpp index 0de1ff5b3..5ee6f031d 100644 --- a/app/timeline/timelinemarker.cpp +++ b/app/timeline/timelinemarker.cpp @@ -55,8 +55,6 @@ void TimelineMarker::set_name(const QString &name) void TimelineMarkerList::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement(QStringLiteral("markers")); - foreach (TimelineMarker* marker, markers_) { writer->writeStartElement(QStringLiteral("marker")); @@ -67,8 +65,6 @@ void TimelineMarkerList::Save(QXmlStreamWriter *writer) const writer->writeEndElement(); // marker } - - writer->writeEndElement(); // markers } TimelineMarkerList::~TimelineMarkerList() diff --git a/app/timeline/timelinepoints.cpp b/app/timeline/timelinepoints.cpp index d1b6e43ec..ba8c6e4e8 100644 --- a/app/timeline/timelinepoints.cpp +++ b/app/timeline/timelinepoints.cpp @@ -54,13 +54,13 @@ void TimelinePoints::Load(QXmlStreamReader *reader) void TimelinePoints::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement(QStringLiteral("points")); + writer->writeStartElement(QStringLiteral("workarea")); + workarea_.Save(writer); + writer->writeEndElement(); // workarea - workarea_.Save(writer); - - markers_.Save(writer); - - writer->writeEndElement(); // points + writer->writeStartElement(QStringLiteral("markers")); + markers_.Save(writer); + writer->writeEndElement(); // markers } TimelineWorkArea *TimelinePoints::workarea() diff --git a/app/timeline/timelineworkarea.cpp b/app/timeline/timelineworkarea.cpp index f0a4db406..3206d4be3 100644 --- a/app/timeline/timelineworkarea.cpp +++ b/app/timeline/timelineworkarea.cpp @@ -81,13 +81,9 @@ void TimelineWorkArea::Load(QXmlStreamReader *reader) void TimelineWorkArea::Save(QXmlStreamWriter *writer) const { - writer->writeStartElement(QStringLiteral("workarea")); - writer->writeAttribute(QStringLiteral("enabled"), QString::number(workarea_enabled_)); writer->writeAttribute(QStringLiteral("in"), workarea_range_.in().toString()); writer->writeAttribute(QStringLiteral("out"), workarea_range_.out().toString()); - - writer->writeEndElement(); // workarea } const rational &TimelineWorkArea::in() const diff --git a/app/widget/nodecopypaste/nodecopypaste.cpp b/app/widget/nodecopypaste/nodecopypaste.cpp index c275d7be4..914e9f32c 100644 --- a/app/widget/nodecopypaste/nodecopypaste.cpp +++ b/app/widget/nodecopypaste/nodecopypaste.cpp @@ -23,6 +23,7 @@ #include #include "core.h" +#include "node/factory.h" #include "widget/nodeview/nodeviewundo.h" #include "window/mainwindow/mainwindow.h" @@ -39,10 +40,15 @@ void NodeCopyPasteWidget::CopyNodesToClipboard(const QList &nodes, void writer.writeStartElement(QStringLiteral("olive")); foreach (Node* n, nodes) { + writer.writeStartElement(QStringLiteral("node")); + writer.writeAttribute(QStringLiteral("id"), n->id()); n->Save(&writer); + writer.writeEndElement(); // node } + writer.writeStartElement(QStringLiteral("custom")); CopyNodesToClipboardInternal(&writer, userdata); + writer.writeEndElement(); // custom writer.writeEndElement(); // olive writer.writeEndDocument(); @@ -67,15 +73,24 @@ QList NodeCopyPasteWidget::PasteNodesFromClipboard(Sequence *graph, QUnd if (reader.name() == QStringLiteral("olive")) { while (XMLReadNextStartElement(&reader)) { if (reader.name() == QStringLiteral("node")) { - Node* node = XMLLoadNode(&reader); + Node* node = nullptr; + + XMLAttributeLoop((&reader), attr) { + if (attr.name() == QStringLiteral("id")) { + node = NodeFactory::CreateFromID(attr.value().toString()); + break; + } + } if (node) { node->Load(&reader, xml_node_data, nullptr); pasted_nodes.append(node); } + } else if (reader.name() == QStringLiteral("custom")) { + PasteNodesFromClipboardInternal(&reader, xml_node_data, userdata); } else { - PasteNodesFromClipboardInternal(&reader, userdata); + reader.skipCurrentElement(); } } } else { @@ -157,7 +172,7 @@ void NodeCopyPasteWidget::CopyNodesToClipboardInternal(QXmlStreamWriter*, void*) { } -void NodeCopyPasteWidget::PasteNodesFromClipboardInternal(QXmlStreamReader* reader, void*) +void NodeCopyPasteWidget::PasteNodesFromClipboardInternal(QXmlStreamReader* reader, XMLNodeData &xml_node_data, void*) { reader->skipCurrentElement(); } diff --git a/app/widget/nodecopypaste/nodecopypaste.h b/app/widget/nodecopypaste/nodecopypaste.h index 299a1ac7e..9791ae99f 100644 --- a/app/widget/nodecopypaste/nodecopypaste.h +++ b/app/widget/nodecopypaste/nodecopypaste.h @@ -41,7 +41,7 @@ protected: virtual void CopyNodesToClipboardInternal(QXmlStreamWriter *writer, void* userdata); - virtual void PasteNodesFromClipboardInternal(QXmlStreamReader *reader, void* userdata); + virtual void PasteNodesFromClipboardInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data, void* userdata); }; diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 12d7bc0c4..acfb8f18e 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -285,8 +285,6 @@ 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; @@ -314,38 +312,32 @@ void TimelineWidget::CopyNodesToClipboardInternal(QXmlStreamWriter *writer, void writer->writeEndElement(); } - - writer->writeEndElement(); // timeline } -void TimelineWidget::PasteNodesFromClipboardInternal(QXmlStreamReader *reader, void *userdata) +void TimelineWidget::PasteNodesFromClipboardInternal(QXmlStreamReader *reader, XMLNodeData& xml_node_data, void *userdata) { - if (reader->name() == QStringLiteral("timeline")) { - QList& paste_data = *static_cast*>(userdata); + QList& paste_data = *static_cast*>(userdata); - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("block")) { - BlockPasteData bpd; + 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(); - } + foreach (QXmlStreamAttribute attr, reader->attributes()) { + if (attr.name() == QStringLiteral("ptr")) { + bpd.block = static_cast(xml_node_data.node_ptrs.value(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(); } + + paste_data.append(bpd); + + reader->skipCurrentElement(); } - } else { - NodeCopyPasteWidget::PasteNodesFromClipboardInternal(reader, userdata); } } @@ -681,12 +673,7 @@ void TimelineWidget::Paste(bool insert) rational paste_end = GetTime(); 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; - } - } + paste_end = qMax(paste_end, paste_start + bpd.in + bpd.block->length()); } if (paste_end != paste_start) { @@ -695,17 +682,12 @@ void TimelineWidget::Paste(bool insert) } 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; - } - } + qDebug() << "Placing" << bpd.block; + new TrackPlaceBlockCommand(GetConnectedNode()->track_list(bpd.track_type), + bpd.track_index, + bpd.block, + paste_start + bpd.in, + command); } Core::instance()->undo_stack()->pushIfHasChildren(command); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 2602aa97c..70cccebad 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -119,10 +119,10 @@ protected: virtual void DisconnectNodeInternal(ViewerOutput* n) override; virtual void CopyNodesToClipboardInternal(QXmlStreamWriter *writer, void* userdata) override; - virtual void PasteNodesFromClipboardInternal(QXmlStreamReader *reader, void* userdata) override; + virtual void PasteNodesFromClipboardInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data, void* userdata) override; struct BlockPasteData { - quintptr ptr; + Block* block; rational in; Timeline::TrackType track_type; int track_index;