From 3f0768a1cc87bcf0d70020dc42ed96514b69ea30 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Sun, 8 Nov 2020 12:23:37 +0000 Subject: [PATCH 01/45] Add support for transitions when loading OTIO files OTIO transitions are now added to the timeline as cross dissolves with the appropriate in/out points, Also added a safety check when dealing with media references. --- app/task/project/loadotio/loadotio.cpp | 62 ++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp index 78799e389..861cc0ba5 100644 --- a/app/task/project/loadotio/loadotio.cpp +++ b/app/task/project/loadotio/loadotio.cpp @@ -25,10 +25,12 @@ #include #include #include +#include #include #include "node/block/clip/clip.h" #include "node/block/gap/gap.h" +#include "node/block/transition/crossdissolve/crossdissolvetransition.h" #include "node/input/media/audio/audio.h" #include "node/input/media/video/video.h" #include "project/item/folder/folder.h" @@ -122,6 +124,9 @@ bool LoadOTIOTask::Run() return false; } + Block* previous_block = nullptr; + bool prev_block_transition = false; + for (auto otio_block_retainer : clip_map) { auto otio_block = otio_block_retainer.value; @@ -136,27 +141,66 @@ bool LoadOTIOTask::Run() block = new GapBlock(); + } else if (otio_block->schema_name() == "Transition") { + + // Todo: Look into OTIO supported transitions and add them to Olive + block = new CrossDissolveTransition(); + } else { // We don't know what this is yet, just create a gap for now so that *something* is there qWarning() << "Found unknown block type:" << otio_block->schema_name().c_str(); block = new GapBlock(); - } - block->SetLabel(QString::fromStdString(otio_block->name())); - - rational start_time = rational::fromDouble(static_cast(otio_block)->source_range()->start_time().to_seconds()); - rational duration = rational::fromDouble(static_cast(otio_block)->source_range()->duration().to_seconds()); - - block->set_media_in(start_time); - block->set_length_and_media_out(duration); sequence->AddNode(block); + + block->SetLabel(QString::fromStdString(otio_block->name())); + rational start_time; + rational duration; + + if (otio_block->schema_name() == "Clip" || otio_block->schema_name() == "Gap") { + start_time = + rational::fromDouble(static_cast(otio_block)->source_range()->start_time().to_seconds()); + duration = + rational::fromDouble(static_cast(otio_block)->source_range()->duration().to_seconds()); + + block->set_media_in(start_time); + block->set_length_and_media_out(duration); + } + + // If the previous block was a transition, connect the current block to it + if (prev_block_transition) { + TransitionBlock* previous_transition_block = static_cast(previous_block); + NodeParam::ConnectEdge(block->output(), previous_transition_block->in_block_input()); + prev_block_transition = false; + } + + if (otio_block->schema_name() == "Transition") { + TransitionBlock* transition_block = static_cast(block); + OTIO::Transition* otio_block_transition = static_cast(otio_block); + + duration = rational::fromDouble((otio_block_transition->in_offset() + otio_block_transition->out_offset()).to_seconds()); + transition_block->set_length_and_media_out(duration); + + if (previous_block) { + NodeParam::ConnectEdge(previous_block->output(), transition_block->out_block_input()); + + // Set how far the transition eats into the previous clip + transition_block->set_media_in(rational::fromDouble(-otio_block_transition->out_offset().to_seconds())); + } + prev_block_transition = true; + } + track->AppendBlock(block); + // Update this after it's used but before any continue statements + previous_block = block; if (otio_block->schema_name() == "Clip") { auto otio_clip = static_cast(otio_block); - + if (!otio_clip->media_reference()) { + continue; + } if (otio_clip->media_reference()->schema_name() == "ExternalReference") { // Link footage QString footage_url = QString::fromStdString(static_cast(otio_clip->media_reference())->target_url()); From 2172cf784ca2d7497c0abca4994b28cdb860b6ec Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Wed, 11 Nov 2020 21:53:44 +0000 Subject: [PATCH 02/45] Rational: conversion to opentime take framerate Add a frame rate option to the conversion to opentime rationals as otio generaly expects rationals to be in the form value/framerate. Also add a check to make sure the rational is not in the form 0/0 as this can cause errors with OTIO. --- app/common/rational.cpp | 6 ++++-- app/common/rational.h | 3 ++- app/task/project/saveotio/saveotio.cpp | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/common/rational.cpp b/app/common/rational.cpp index 2160e3f0e..ff44bbd89 100644 --- a/app/common/rational.cpp +++ b/app/common/rational.cpp @@ -103,10 +103,12 @@ AVRational rational::toAVRational() const } #ifdef USE_OTIO -opentime::RationalTime rational::toRationalTime() const +opentime::RationalTime rational::toRationalTime(double framerate) const { // Is this the best way of doing this? - return opentime::RationalTime::from_seconds(toDouble()); + // Olive can store rationals as 0/0 which causes errors in OTIO + opentime::RationalTime time = opentime::RationalTime(numer_, denom_ == 0 ? 1 : denom_); + return time.rescaled_to(framerate); } #endif diff --git a/app/common/rational.h b/app/common/rational.h index 358ea9697..8074c9b07 100644 --- a/app/common/rational.h +++ b/app/common/rational.h @@ -101,7 +101,8 @@ public: AVRational toAVRational() const; #ifdef USE_OTIO - opentime::RationalTime toRationalTime() const; + // Convert Olive ratioanls to opentime rationals with the given framerate (defaults to 24) + opentime::RationalTime toRationalTime(double framerate = 24) const; #endif // Produce "flipped" version diff --git a/app/task/project/saveotio/saveotio.cpp b/app/task/project/saveotio/saveotio.cpp index 0fdb91b61..890d11dfd 100644 --- a/app/task/project/saveotio/saveotio.cpp +++ b/app/task/project/saveotio/saveotio.cpp @@ -144,8 +144,8 @@ opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) } case Block::kGap: { - otio_block = new opentimelineio::v1_0::Gap( - opentimelineio::v1_0::TimeRange(block->in().toRationalTime(), block->length().toRationalTime()), + otio_block = new opentimelineio::v1_0::Gap(opentimelineio::v1_0::TimeRange(block->in().toRationalTime(), + block->length().toRationalTime()), block->GetLabel().toStdString() ); break; From cea436b7031590bbf93fa390115264bb37870a56 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Wed, 11 Nov 2020 21:57:45 +0000 Subject: [PATCH 03/45] saveoitio: Add fix for to_json_file The function to_json_file can delete it's SerializableObject if it doesn't have a Retainer associated with it. This fix adds said Retainer. Retainers clean them selves up when possibly_delete is called on their associated object. --- app/task/project/saveotio/saveotio.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/task/project/saveotio/saveotio.cpp b/app/task/project/saveotio/saveotio.cpp index 890d11dfd..ab3933ed9 100644 --- a/app/task/project/saveotio/saveotio.cpp +++ b/app/task/project/saveotio/saveotio.cpp @@ -24,11 +24,14 @@ #include #include #include +#include #include #include "node/block/transition/transition.h" #include "node/input/media/media.h" +#define OTIO opentimelineio::OPENTIMELINEIO_VERSION + OLIVE_NAMESPACE_ENTER SaveOTIOTask::SaveOTIOTask(ProjectPtr project) : @@ -94,6 +97,7 @@ bool SaveOTIOTask::Run() opentimelineio::v1_0::Timeline *SaveOTIOTask::SerializeTimeline(SequencePtr sequence) { auto otio_timeline = new opentimelineio::v1_0::Timeline(sequence->name().toStdString()); + OTIO::Timeline::Retainer* time_retainer = new OTIO::Timeline::Retainer(otio_timeline); if (!SerializeTrackList(sequence->viewer_output()->track_list(Timeline::kTrackTypeVideo), otio_timeline) || !SerializeTrackList(sequence->viewer_output()->track_list(Timeline::kTrackTypeAudio), otio_timeline)) { From a1d967efe69432d19426b144d17762848cdf6e41 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Wed, 11 Nov 2020 22:13:14 +0000 Subject: [PATCH 04/45] OTIO: Use macro OTIO for opentimelineio::v1_0 Exapnd the use of the OTIO macro and add the version number macro. --- app/task/project/loadotio/loadotio.cpp | 2 +- app/task/project/saveotio/saveotio.cpp | 42 ++++++++++++-------------- app/task/project/saveotio/saveotio.h | 8 +++-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp index 78799e389..25b83658f 100644 --- a/app/task/project/loadotio/loadotio.cpp +++ b/app/task/project/loadotio/loadotio.cpp @@ -34,7 +34,7 @@ #include "project/item/folder/folder.h" #include "project/item/sequence/sequence.h" -#define OTIO opentimelineio::v1_0 +#define OTIO opentimelineio::OPENTIMELINEIO_VERSION OLIVE_NAMESPACE_ENTER diff --git a/app/task/project/saveotio/saveotio.cpp b/app/task/project/saveotio/saveotio.cpp index ab3933ed9..37e8f5f90 100644 --- a/app/task/project/saveotio/saveotio.cpp +++ b/app/task/project/saveotio/saveotio.cpp @@ -30,8 +30,6 @@ #include "node/block/transition/transition.h" #include "node/input/media/media.h" -#define OTIO opentimelineio::OPENTIMELINEIO_VERSION - OLIVE_NAMESPACE_ENTER SaveOTIOTask::SaveOTIOTask(ProjectPtr project) : @@ -49,7 +47,7 @@ bool SaveOTIOTask::Run() return false; } - std::vector serialized; + std::vector serialized; foreach (ItemPtr item, sequences) { SequencePtr seq = std::static_pointer_cast(sequences.first()); @@ -72,7 +70,7 @@ bool SaveOTIOTask::Run() } } - opentimelineio::v1_0::ErrorStatus es; + OTIO::ErrorStatus es; if (serialized.size() == 1) { // Serialize timeline on its own @@ -81,7 +79,7 @@ bool SaveOTIOTask::Run() t->possibly_delete(); } else { // Serialize all into a SerializableCollection - auto collection = new opentimelineio::v1_0::SerializableCollection("Sequences", serialized); + auto collection = new OTIO::SerializableCollection("Sequences", serialized); collection->to_json_file(project_->filename().toStdString(), &es); collection->possibly_delete(); @@ -91,12 +89,12 @@ bool SaveOTIOTask::Run() } } - return (es == opentimelineio::v1_0::ErrorStatus::OK); + return (es == OTIO::ErrorStatus::OK); } -opentimelineio::v1_0::Timeline *SaveOTIOTask::SerializeTimeline(SequencePtr sequence) +OTIO::Timeline *SaveOTIOTask::SerializeTimeline(SequencePtr sequence) { - auto otio_timeline = new opentimelineio::v1_0::Timeline(sequence->name().toStdString()); + auto otio_timeline = new OTIO::Timeline(sequence->name().toStdString()); OTIO::Timeline::Retainer* time_retainer = new OTIO::Timeline::Retainer(otio_timeline); if (!SerializeTrackList(sequence->viewer_output()->track_list(Timeline::kTrackTypeVideo), otio_timeline) @@ -108,11 +106,11 @@ opentimelineio::v1_0::Timeline *SaveOTIOTask::SerializeTimeline(SequencePtr sequ return otio_timeline; } -opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) +OTIO::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) { - auto otio_track = new opentimelineio::v1_0::Track(); + auto otio_track = new OTIO::Track(); - opentimelineio::v1_0::ErrorStatus es; + OTIO::ErrorStatus es; switch (track->track_type()) { case Timeline::kTrackTypeVideo: @@ -127,19 +125,19 @@ opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) } foreach (Block* block, track->Blocks()) { - opentimelineio::v1_0::Composable* otio_block = nullptr; + OTIO::Composable* otio_block = nullptr; switch (block->type()) { case Block::kClip: { - auto otio_clip = new opentimelineio::v1_0::Clip(block->GetLabel().toStdString()); + auto otio_clip = new OTIO::Clip(block->GetLabel().toStdString()); - otio_clip->set_source_range(opentimelineio::v1_0::TimeRange(block->in().toRationalTime(), + otio_clip->set_source_range(OTIO::TimeRange(block->in().toRationalTime(), block->length().toRationalTime())); QList media_nodes = block->FindInputNodes(); if (!media_nodes.isEmpty()) { - auto media_ref = new opentimelineio::v1_0::ExternalReference(media_nodes.first()->stream()->footage()->filename().toStdString()); + auto media_ref = new OTIO::ExternalReference(media_nodes.first()->stream()->footage()->filename().toStdString()); otio_clip->set_media_reference(media_ref); } @@ -148,7 +146,7 @@ opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) } case Block::kGap: { - otio_block = new opentimelineio::v1_0::Gap(opentimelineio::v1_0::TimeRange(block->in().toRationalTime(), + otio_block = new OTIO::Gap(OTIO::TimeRange(block->in().toRationalTime(), block->length().toRationalTime()), block->GetLabel().toStdString() ); @@ -156,14 +154,14 @@ opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) } case Block::kTransition: { - auto otio_transition = new opentimelineio::v1_0::Transition(block->GetLabel().toStdString()); + auto otio_transition = new OTIO::Transition(block->GetLabel().toStdString()); TransitionBlock* our_transition = static_cast(block); otio_transition->set_in_offset(our_transition->in_offset().toRationalTime()); otio_transition->set_out_offset(our_transition->out_offset().toRationalTime()); - otio_block = new opentimelineio::v1_0::Transition(); + otio_block = new OTIO::Transition(); break; } } @@ -175,7 +173,7 @@ opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) otio_track->append_child(otio_block, &es); - if (es != opentimelineio::v1_0::ErrorStatus::OK) { + if (es != OTIO::ErrorStatus::OK) { goto fail; } } @@ -188,9 +186,9 @@ fail: return nullptr; } -bool SaveOTIOTask::SerializeTrackList(TrackList *list, opentimelineio::v1_0::Timeline* otio_timeline) +bool SaveOTIOTask::SerializeTrackList(TrackList *list, OTIO::Timeline* otio_timeline) { - opentimelineio::v1_0::ErrorStatus es; + OTIO::ErrorStatus es; foreach (TrackOutput* track, list->GetTracks()) { auto otio_track = SerializeTrack(track); @@ -201,7 +199,7 @@ bool SaveOTIOTask::SerializeTrackList(TrackList *list, opentimelineio::v1_0::Tim otio_timeline->tracks()->append_child(otio_track, &es); - if (es != opentimelineio::v1_0::ErrorStatus::OK) { + if (es != OTIO::ErrorStatus::OK) { otio_track->possibly_delete(); return false; } diff --git a/app/task/project/saveotio/saveotio.h b/app/task/project/saveotio/saveotio.h index 2a380cddc..b939739d9 100644 --- a/app/task/project/saveotio/saveotio.h +++ b/app/task/project/saveotio/saveotio.h @@ -27,6 +27,8 @@ #include "project/project.h" #include "task/task.h" +#define OTIO opentimelineio::OPENTIMELINEIO_VERSION + OLIVE_NAMESPACE_ENTER class SaveOTIOTask : public Task @@ -39,11 +41,11 @@ protected: virtual bool Run() override; private: - opentimelineio::v1_0::Timeline* SerializeTimeline(SequencePtr sequence); + OTIO::Timeline* SerializeTimeline(SequencePtr sequence); - opentimelineio::v1_0::Track* SerializeTrack(TrackOutput* track); + OTIO::Track* SerializeTrack(TrackOutput* track); - bool SerializeTrackList(TrackList* list, opentimelineio::v1_0::Timeline *otio_timeline); + bool SerializeTrackList(TrackList* list, OTIO::Timeline *otio_timeline); ProjectPtr project_; From 7f4b6763188be326b88e3301696546bf808a9ace Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Thu, 12 Nov 2020 13:44:50 +0000 Subject: [PATCH 05/45] Move OTIO macro to define.h Move the OTIO macro to define.h to avoid having to add it to all required files. Also add some cleanup. --- app/common/define.h | 2 ++ app/task/project/loadotio/loadotio.cpp | 2 -- app/task/project/saveotio/saveotio.cpp | 6 +++--- app/task/project/saveotio/saveotio.h | 2 -- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/common/define.h b/app/common/define.h index aa968fcf7..4421b9f3a 100644 --- a/app/common/define.h +++ b/app/common/define.h @@ -27,6 +27,8 @@ #define OLIVE_NAMESPACE_EXIT } +#define OTIO opentimelineio::OPENTIMELINEIO_VERSION + OLIVE_NAMESPACE_ENTER const int kHSVChannels = 3; diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp index 25b83658f..763139560 100644 --- a/app/task/project/loadotio/loadotio.cpp +++ b/app/task/project/loadotio/loadotio.cpp @@ -34,8 +34,6 @@ #include "project/item/folder/folder.h" #include "project/item/sequence/sequence.h" -#define OTIO opentimelineio::OPENTIMELINEIO_VERSION - OLIVE_NAMESPACE_ENTER LoadOTIOTask::LoadOTIOTask(const QString& s) : diff --git a/app/task/project/saveotio/saveotio.cpp b/app/task/project/saveotio/saveotio.cpp index 37e8f5f90..bc706fcef 100644 --- a/app/task/project/saveotio/saveotio.cpp +++ b/app/task/project/saveotio/saveotio.cpp @@ -95,7 +95,7 @@ bool SaveOTIOTask::Run() OTIO::Timeline *SaveOTIOTask::SerializeTimeline(SequencePtr sequence) { auto otio_timeline = new OTIO::Timeline(sequence->name().toStdString()); - OTIO::Timeline::Retainer* time_retainer = new OTIO::Timeline::Retainer(otio_timeline); + OTIO::Timeline::Retainer* timeline_retainer = new OTIO::Timeline::Retainer(otio_timeline); if (!SerializeTrackList(sequence->viewer_output()->track_list(Timeline::kTrackTypeVideo), otio_timeline) || !SerializeTrackList(sequence->viewer_output()->track_list(Timeline::kTrackTypeAudio), otio_timeline)) { @@ -147,8 +147,8 @@ OTIO::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) case Block::kGap: { otio_block = new OTIO::Gap(OTIO::TimeRange(block->in().toRationalTime(), - block->length().toRationalTime()), - block->GetLabel().toStdString() + block->length().toRationalTime()), + block->GetLabel().toStdString() ); break; } diff --git a/app/task/project/saveotio/saveotio.h b/app/task/project/saveotio/saveotio.h index b939739d9..69338f16a 100644 --- a/app/task/project/saveotio/saveotio.h +++ b/app/task/project/saveotio/saveotio.h @@ -27,8 +27,6 @@ #include "project/project.h" #include "task/task.h" -#define OTIO opentimelineio::OPENTIMELINEIO_VERSION - OLIVE_NAMESPACE_ENTER class SaveOTIOTask : public Task From 3b68f027f879c854df885e959730f7afa57ec9a7 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Thu, 12 Nov 2020 13:48:24 +0000 Subject: [PATCH 06/45] More cleanup --- app/task/project/saveotio/saveotio.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/task/project/saveotio/saveotio.cpp b/app/task/project/saveotio/saveotio.cpp index bc706fcef..a8a95da33 100644 --- a/app/task/project/saveotio/saveotio.cpp +++ b/app/task/project/saveotio/saveotio.cpp @@ -95,6 +95,7 @@ bool SaveOTIOTask::Run() OTIO::Timeline *SaveOTIOTask::SerializeTimeline(SequencePtr sequence) { auto otio_timeline = new OTIO::Timeline(sequence->name().toStdString()); + // Retainers clean themselves up when the final user is removed OTIO::Timeline::Retainer* timeline_retainer = new OTIO::Timeline::Retainer(otio_timeline); if (!SerializeTrackList(sequence->viewer_output()->track_list(Timeline::kTrackTypeVideo), otio_timeline) @@ -149,7 +150,7 @@ OTIO::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) otio_block = new OTIO::Gap(OTIO::TimeRange(block->in().toRationalTime(), block->length().toRationalTime()), block->GetLabel().toStdString() - ); + ); break; } case Block::kTransition: From f9e2a83ee4f4726bbf7df06dfa93f5326df97a29 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Mon, 16 Nov 2020 16:47:31 +0000 Subject: [PATCH 07/45] Create OTIO namespace in otioutils.h --- app/common/CMakeLists.txt | 1 + app/common/define.h | 1 - app/common/otioutils.h | 29 ++++++++++++++++++++++++++++ app/task/project/loadotio/loadotio.h | 2 ++ app/task/project/saveotio/saveotio.h | 1 + 5 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 app/common/otioutils.h diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index 99521453c..5c2daad49 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -37,6 +37,7 @@ set(OLIVE_SOURCES common/lerp.h common/memorypool.h common/memorypool.cpp + common/otioutils.h common/qtutils.h common/qtutils.cpp common/range.h diff --git a/app/common/define.h b/app/common/define.h index 4421b9f3a..28c400b8b 100644 --- a/app/common/define.h +++ b/app/common/define.h @@ -27,7 +27,6 @@ #define OLIVE_NAMESPACE_EXIT } -#define OTIO opentimelineio::OPENTIMELINEIO_VERSION OLIVE_NAMESPACE_ENTER diff --git a/app/common/otioutils.h b/app/common/otioutils.h new file mode 100644 index 000000000..1a3a32dd0 --- /dev/null +++ b/app/common/otioutils.h @@ -0,0 +1,29 @@ +/*** + + 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 OTIOUTILS_H +#define OTIOUTILS_H + +#ifdef USE_OTIO +#include +namespace OTIO = opentimelineio::OPENTIMELINEIO_VERSION; +#endif + +#endif // OTIOUTILS diff --git a/app/task/project/loadotio/loadotio.h b/app/task/project/loadotio/loadotio.h index 5e44bb67d..639884310 100644 --- a/app/task/project/loadotio/loadotio.h +++ b/app/task/project/loadotio/loadotio.h @@ -21,6 +21,8 @@ #ifndef OTIODECODER_H #define OTIODECODER_H + +#include "common/otioutils.h" #include "project/project.h" #include "task/project/load/loadbasetask.h" diff --git a/app/task/project/saveotio/saveotio.h b/app/task/project/saveotio/saveotio.h index 69338f16a..a360dab5d 100644 --- a/app/task/project/saveotio/saveotio.h +++ b/app/task/project/saveotio/saveotio.h @@ -24,6 +24,7 @@ #include #include +#include "common/otioutils.h" #include "project/project.h" #include "task/task.h" From 8433e4093a082de47636b6eb4b5410f248134fa2 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Mon, 16 Nov 2020 16:57:08 +0000 Subject: [PATCH 08/45] Cleanup --- app/common/define.h | 1 - app/task/project/loadotio/loadotio.h | 1 - app/task/project/saveotio/saveotio.cpp | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/common/define.h b/app/common/define.h index 28c400b8b..aa968fcf7 100644 --- a/app/common/define.h +++ b/app/common/define.h @@ -27,7 +27,6 @@ #define OLIVE_NAMESPACE_EXIT } - OLIVE_NAMESPACE_ENTER const int kHSVChannels = 3; diff --git a/app/task/project/loadotio/loadotio.h b/app/task/project/loadotio/loadotio.h index 639884310..da156f68c 100644 --- a/app/task/project/loadotio/loadotio.h +++ b/app/task/project/loadotio/loadotio.h @@ -21,7 +21,6 @@ #ifndef OTIODECODER_H #define OTIODECODER_H - #include "common/otioutils.h" #include "project/project.h" #include "task/project/load/loadbasetask.h" diff --git a/app/task/project/saveotio/saveotio.cpp b/app/task/project/saveotio/saveotio.cpp index a8a95da33..586bc8570 100644 --- a/app/task/project/saveotio/saveotio.cpp +++ b/app/task/project/saveotio/saveotio.cpp @@ -134,7 +134,7 @@ OTIO::Track *SaveOTIOTask::SerializeTrack(TrackOutput *track) auto otio_clip = new OTIO::Clip(block->GetLabel().toStdString()); otio_clip->set_source_range(OTIO::TimeRange(block->in().toRationalTime(), - block->length().toRationalTime())); + block->length().toRationalTime())); QList media_nodes = block->FindInputNodes(); if (!media_nodes.isEmpty()) { From 0cf640fb094550e84599d74adf87725e3d44d9aa Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Fri, 4 Dec 2020 19:20:42 +0000 Subject: [PATCH 09/45] Suppress unused variable warning. --- app/task/project/saveotio/saveotio.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/task/project/saveotio/saveotio.cpp b/app/task/project/saveotio/saveotio.cpp index b477b4ec5..7143d05e2 100644 --- a/app/task/project/saveotio/saveotio.cpp +++ b/app/task/project/saveotio/saveotio.cpp @@ -97,6 +97,8 @@ OTIO::Timeline *SaveOTIOTask::SerializeTimeline(SequencePtr sequence) auto otio_timeline = new OTIO::Timeline(sequence->name().toStdString()); // Retainers clean themselves up when the final user is removed OTIO::Timeline::Retainer* timeline_retainer = new OTIO::Timeline::Retainer(otio_timeline); + // Suppress unused variable warning + Q_UNUSED(timeline_retainer); if (!SerializeTrackList(sequence->viewer_output()->track_list(Timeline::kTrackTypeVideo), otio_timeline) || !SerializeTrackList(sequence->viewer_output()->track_list(Timeline::kTrackTypeAudio), otio_timeline)) { From ec3ee9e964f33f5aebafc45663244d05a8331b48 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Apr 2021 20:00:28 +1000 Subject: [PATCH 10/45] fix symbol discovery on macOS --- app/dialog/crashhandler/crashhandler.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/dialog/crashhandler/crashhandler.cpp b/app/dialog/crashhandler/crashhandler.cpp index 3f31748de..ee2a36877 100644 --- a/app/dialog/crashhandler/crashhandler.cpp +++ b/app/dialog/crashhandler/crashhandler.cpp @@ -233,11 +233,17 @@ void CrashHandlerDialog::SendErrorReport() // Find symbol file QDir symbol_dir(GetSymbolPath()); -#ifdef Q_OS_WINDOWS - symbol_dir = QDir(symbol_dir.filePath(QStringLiteral("olive-editor.pdb"))); + + QString symbol_bin_name; +#if defined(OS_WIN) + symbol_bin_name = QStringLiteral("olive-editor.pdb"); +#elif defined(OS_APPLE) + symbol_bin_name = QStringLiteral("Olive"); #else - symbol_dir = QDir(symbol_dir.filePath(QStringLiteral("olive-editor"))); + symbol_bin_name = QStringLiteral("olive-editor"); #endif + symbol_dir = QDir(symbol_dir.filePath(symbol_bin_name)); + QStringList folders_in_symbol_path = symbol_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); if (folders_in_symbol_path.size() > 0) { @@ -250,7 +256,12 @@ void CrashHandlerDialog::SendErrorReport() } // Create sym section - QString symbol_filename = QStringLiteral("olive-editor.sym"); + QString symbol_filename +#if defined(OS_APPLE) + symbol_filename = QStringLiteral("Olive.sym"); +#else + symbol_filename = QStringLiteral("olive-editor.sym"); +#endif QString symbol_full_path = symbol_dir.filePath(symbol_filename); QHttpPart sym_part; sym_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/octet-stream")); From 4021a7166d1855b256a9f71be09a134e6b7ec009 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Apr 2021 22:37:38 +1000 Subject: [PATCH 11/45] viewer: keep track of which screens have full screen viewers on them Fixes #1307 --- app/widget/viewer/viewer.cpp | 13 ++++++++++--- app/widget/viewer/viewer.h | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index cf0113ce2..a4e6fa77f 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -129,7 +129,7 @@ ViewerWidget::~ViewerWidget() { instances_.removeOne(this); - QList windows = windows_; + auto windows = windows_; foreach (ViewerWindow* window, windows) { delete window; @@ -311,6 +311,11 @@ void ViewerWidget::SetFullScreen(QScreen *screen) } } + if (windows_.contains(screen)) { + delete windows_.take(screen); + return; + } + ViewerWindow* vw = new ViewerWindow(this); vw->setGeometry(screen->geometry()); @@ -326,7 +331,7 @@ void ViewerWidget::SetFullScreen(QScreen *screen) vw->display_widget()->SetImage(display_widget_->last_loaded_buffer()); - windows_.append(vw); + windows_.insert(screen, vw); } void ViewerWidget::ForceUpdate() @@ -745,7 +750,7 @@ void ViewerWidget::ContextMenuSetCustomSafeMargins() void ViewerWidget::WindowAboutToClose() { - windows_.removeOne(static_cast(sender())); + windows_.remove(windows_.key(static_cast(sender()))); } void ViewerWidget::ContextMenuScopeTriggered(QAction *action) @@ -854,6 +859,8 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) QString::number(s->size().height()))); a->setData(i); + a->setCheckable(true); + a->setChecked(windows_.contains(QGuiApplication::screens().at(i))); } connect(full_screen_menu, &QMenu::triggered, this, &ViewerWidget::ContextMenuSetFullScreen); diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 5eeb4f636..c3f35f505 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -223,7 +223,7 @@ private: AudioWaveformView* waveform_view_; - QList windows_; + QHash windows_; ViewerDisplayWidget* display_widget_; From 11548b3a74336e8474472020dfa99ea53fff4f64 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Apr 2021 22:38:42 +1000 Subject: [PATCH 12/45] added missing semicolon --- app/dialog/crashhandler/crashhandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/dialog/crashhandler/crashhandler.cpp b/app/dialog/crashhandler/crashhandler.cpp index ee2a36877..7365983ca 100644 --- a/app/dialog/crashhandler/crashhandler.cpp +++ b/app/dialog/crashhandler/crashhandler.cpp @@ -256,7 +256,7 @@ void CrashHandlerDialog::SendErrorReport() } // Create sym section - QString symbol_filename + QString symbol_filename; #if defined(OS_APPLE) symbol_filename = QStringLiteral("Olive.sym"); #else From e037db2f7747e254bd5d7f600a2d5f90e701d706 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Apr 2021 22:57:09 +1000 Subject: [PATCH 13/45] autocacher: re-copy list before waiting --- app/render/previewautocacher.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 6189521fa..66a8c0f8a 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -386,6 +386,10 @@ void PreviewAutoCacher::ClearVideoQueue(bool wait) watcher->Cancel(); } if (wait) { + // Re-copy because the above cancels may have deleted these watchers + vt_copy = video_tasks_; + sft_copy = single_frame_tasks_; + for (auto it=vt_copy.cbegin(); it!=vt_copy.cend(); it++) { it.key()->WaitForFinished(); } From 831bac70c5d4301ca9c124476496a01a7906bf06 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Apr 2021 23:33:55 +1000 Subject: [PATCH 14/45] viewer: don't trigger context menu if no node is connected --- app/widget/viewer/viewer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index a4e6fa77f..8a8a229d7 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -806,6 +806,10 @@ void ViewerWidget::RendererGeneratedFrameForQueue() void ViewerWidget::ShowContextMenu(const QPoint &pos) { + if (!GetConnectedNode()) { + return; + } + Menu menu(static_cast(sender())); context_menu_widget_ = dynamic_cast(sender()); From 12b190346d2e55f7cf4b35e263347cd2f483998c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Apr 2021 23:38:44 +1000 Subject: [PATCH 15/45] cache: clear tasks if wait is enabled --- app/render/previewautocacher.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 66a8c0f8a..ed681955b 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -370,6 +370,8 @@ void PreviewAutoCacher::ClearHashQueue(bool wait) for (auto it=copy.cbegin(); it!=copy.cend(); it++) { (*it)->waitForFinished(); } + + hash_tasks_.clear(); } } @@ -396,6 +398,11 @@ void PreviewAutoCacher::ClearVideoQueue(bool wait) foreach (RenderTicketWatcher* watcher, sft_copy) { watcher->WaitForFinished(); } + + // If we're waiting, we prioritize clearing the cache. Otherwise, we assume that tasks can still + // finish after this function returns. + video_tasks_.clear(); + single_frame_tasks_.clear(); } has_changed_ = true; @@ -415,6 +422,8 @@ void PreviewAutoCacher::ClearAudioQueue(bool wait) for (auto it=copy.cbegin(); it!=copy.cend(); it++) { it.key()->WaitForFinished(); } + + audio_tasks_.clear(); } } @@ -431,6 +440,8 @@ void PreviewAutoCacher::ClearVideoDownloadQueue(bool wait) for (auto it=copy.cbegin(); it!=copy.cend(); it++) { it.key()->WaitForFinished(); } + + video_download_tasks_.clear(); } } From 8f21d05baac48d5a0651a6ccc223532c35076e94 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 8 Apr 2021 23:44:39 +1000 Subject: [PATCH 16/45] more robust panel closing --- app/window/mainwindow/mainwindow.cpp | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index ed69336c4..4cdf2e1d3 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -331,21 +331,20 @@ void MainWindow::ProjectOpen(Project *p) void MainWindow::ProjectClose(Project *p) { - // Close any open sequences from project - QVector open_sequences = p->root()->ListChildrenOfType(); + // Close any nodes open in TimeBasedWidgets + foreach (PanelWidget* panel, PanelManager::instance()->panels()) { + TimeBasedPanel* tbp = dynamic_cast(panel); - foreach (Sequence* seq, open_sequences) { - if (IsSequenceOpen(seq)) { - CloseSequence(seq); + if (tbp && tbp->GetConnectedViewer() && tbp->GetConnectedViewer()->project() == p) { + if (dynamic_cast(tbp)) { + // Prefer our CloseSequence function which will delete any unnecessary timeline panels + CloseSequence(static_cast(tbp->GetConnectedViewer())); + } else { + tbp->DisconnectViewerNode(); + } } } - // Close any open footage in footage viewer - if (footage_viewer_panel_->GetConnectedViewer() - && footage_viewer_panel_->GetConnectedViewer()->project() == p) { - footage_viewer_panel_->DisconnectViewerNode(); - } - // Close any extra folder panels foreach (ProjectPanel* panel, folder_panels_) { if (panel->project() == p) { @@ -523,11 +522,9 @@ void MainWindow::RemoveTimelinePanel(TimelinePanel *panel) { // Stop showing this timeline in the viewer TimelineFocused(nullptr); + panel->ConnectViewerNode(nullptr); - if (timeline_panels_.size() == 1) { - // Leave our single remaining timeline panel open - panel->ConnectViewerNode(nullptr); - } else { + if (timeline_panels_.size() != 1) { timeline_panels_.removeOne(panel); panel->deleteLater(); } From 0cb8da3de1f438cd37632f81833e48e8b763803e Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 9 Apr 2021 08:47:09 +1000 Subject: [PATCH 17/45] use WindowModal on all crash handler messageboxes Improves appearance on macOS. --- app/dialog/crashhandler/crashhandler.cpp | 62 +++++++++++++++++------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/app/dialog/crashhandler/crashhandler.cpp b/app/dialog/crashhandler/crashhandler.cpp index 7365983ca..0c4ab384a 100644 --- a/app/dialog/crashhandler/crashhandler.cpp +++ b/app/dialog/crashhandler/crashhandler.cpp @@ -148,9 +148,14 @@ void CrashHandlerDialog::ReplyFinished(QNetworkReply* reply) // Close dialog QDialog::accept(); } else { - QMessageBox::critical(this, tr("Upload Failed"), - tr("Failed to send error report. Please try again later."), - QMessageBox::Ok); + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Upload Failed")); + b.setText(tr("Failed to send error report. Please try again later.")); + b.addButton(QMessageBox::Ok); + b.exec(); + SetGUIObjectsEnabled(true); } } @@ -181,10 +186,15 @@ void CrashHandlerDialog::ReadProcessFinished() void CrashHandlerDialog::SendErrorReport() { if (summary_edit_->document()->isEmpty()) { - if (QMessageBox::question(this, - tr("No Crash Summary"), - tr("Are you sure you want to send an error report with no crash summary?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + QMessageBox b(this); + b.setIcon(QMessageBox::Question); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("No Crash Summary")); + b.setText(tr("Are you sure you want to send an error report with no crash summary?")); + b.addButton(QMessageBox::Yes); + b.addButton(QMessageBox::No); + + if (b.exec() == QMessageBox::No) { return; } } @@ -249,9 +259,15 @@ void CrashHandlerDialog::SendErrorReport() if (folders_in_symbol_path.size() > 0) { symbol_dir = QDir(symbol_dir.filePath(folders_in_symbol_path.first())); } else { - QMessageBox::critical(this, tr("Failed to send report"), tr("Failed to find symbols necessary to send report. " - "This is a packaging issue. Please notify " - "the maintainers of this package.")); + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Failed to send report")); + b.setText(tr("Failed to find symbols necessary to send report. " + "This is a packaging issue. Please notify " + "the maintainers of this package.")); + b.addButton(QMessageBox::Ok); + b.exec(); return; } @@ -270,8 +286,14 @@ void CrashHandlerDialog::SendErrorReport() QFile sym_file(symbol_full_path); if (!sym_file.open(QFile::ReadOnly)) { - QMessageBox::critical(this, tr("Failed to send report"), tr("Failed to open symbol file. You may not have " - "permission to access it.")); + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Failed to send report")); + b.setText(tr("Failed to open symbol file. You may not have " + "permission to access it.")); + b.addButton(QMessageBox::Ok); + b.exec(); return; } @@ -289,12 +311,16 @@ void CrashHandlerDialog::SendErrorReport() void CrashHandlerDialog::closeEvent(QCloseEvent* e) { - if (waiting_for_upload_ - && QMessageBox::warning(this, - tr("Confirm Close"), - tr("Crash report is still uploading. Closing now may result in no " - "report being sent. Are you sure you wish to close?"), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) { + QMessageBox b(this); + b.setIcon(QMessageBox::Warning); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Confirm Close")); + b.setText(tr("Crash report is still uploading. Closing now may result in no " + "report being sent. Are you sure you wish to close?")); + b.addButton(QMessageBox::Ok); + b.addButton(QMessageBox::Cancel); + + if (waiting_for_upload_ && b.exec() == QMessageBox::Cancel) { e->ignore(); } else { e->accept(); From 1dc23dae942752848a914ed353c7b5d49ec86762 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 9 Apr 2021 08:48:57 +1000 Subject: [PATCH 18/45] precachetask: remove assert and connect correctly --- app/task/precache/precachetask.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp index 536e47d2e..28d08954e 100644 --- a/app/task/precache/precachetask.cpp +++ b/app/task/precache/precachetask.cpp @@ -30,17 +30,12 @@ PreCacheTask::PreCacheTask(Footage *footage, int index, Sequence* sequence) : viewer()->SetVideoParams(sequence->GetVideoParams()); viewer()->SetAudioParams(sequence->GetAudioParams()); - // FIXME: I've been lazy and haven't included support for anything connected to a footage input. - // At the moment, footage nodes have no connectable inputs so it's not a problem, but if - // they ever do, that needs to be addressed immediately. - Q_ASSERT(footage->inputs().isEmpty()); - // Copy footage node so it can precache without any modifications from the user screwing it up footage_ = static_cast(footage->copy()); index_ = index; Node::CopyInputs(footage, footage_, false); - Node::ConnectEdge(footage_, NodeInput(viewer(), ViewerOutput::kTextureInput)); + Node::ConnectEdge(NodeOutput(footage_, Track::Reference(Track::kVideo, 0).ToString()), NodeInput(viewer(), ViewerOutput::kTextureInput)); SetTitle(tr("Pre-caching %1:%2").arg(footage_->filename())); } From 94282f3ac6f7be54ef3c015e9b41e17f7ee69a1c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 9 Apr 2021 10:16:45 +1000 Subject: [PATCH 19/45] don't use modal dialog for project saving Fixes #1554, however in the future, we could probably make this even better. --- app/core.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/core.cpp b/app/core.cpp index c54283f31..92df5c3e7 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -741,14 +741,22 @@ void Core::SaveProjectInternal(Project* project, const QString& override_filenam } } - TaskDialog* task_dialog = new TaskDialog(psm, tr("Save Project"), main_window_); - - if (override_filename.isEmpty()) { - // Default behavior: set as not modified and push to top of "Open Recent" dialog - connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::ProjectSaveSucceeded); + // We don't use a TaskDialog here because a model save dialog is annoying, particularly when + // saving auto-recoveries that the user can't anticipate. Doing this in the main thread will + // cause a brief (but often unnoticeable) pause in the GUI, which, while not ideal, is not that + // different from what already happened (modal dialog preventing use of the GUI) and in many ways + // less annoying (doesn't disrupt any current actions or pull focus from elsewhere). + // + // Ideally we could do this in a background thread and show progress in the status bar like + // Microsoft Word, but that would be far more complex. If it becomes necessary in the future, + // we will look into an approach like that. + if (psm->Start()) { + if (override_filename.isEmpty()) { + ProjectSaveSucceeded(psm); + } } - task_dialog->open(); + psm->deleteLater(); } ViewerOutput* Core::GetSequenceToExport() From c687460f89537a4b1ef0964af67ed9d0b95b99c7 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 10 Apr 2021 09:24:16 +1000 Subject: [PATCH 20/45] added null checks to timeline snapping and seekablewidget seeking --- app/widget/timelinewidget/timelinewidget.cpp | 4 ++++ app/widget/timeruler/seekablewidget.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 581145055..49f9e5d83 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -1503,6 +1503,10 @@ QVector AttemptSnap(const QVector& screen_pt, bool TimelineWidget::SnapPoint(QVector start_times, rational* movement, int snap_points) { + if (!GetConnectedNode()) { + return false; + } + QVector screen_pt; foreach (const rational& s, start_times) { diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp index 09559a60b..83ceba890 100644 --- a/app/widget/timeruler/seekablewidget.cpp +++ b/app/widget/timeruler/seekablewidget.cpp @@ -160,6 +160,10 @@ int SeekableWidget::TimeToScreen(const rational &time) const void SeekableWidget::SeekToScreenPoint(int screen) { + if (timebase().isNull()) { + return; + } + int64_t timestamp = qMax(static_cast(0), ScreenToUnitRounded(screen)); if (Core::instance()->snapping() && snap_service_) { From 792b3b7a4457969c57c4d15ff1780f7a1fdff603 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 10 Apr 2021 13:22:05 +1000 Subject: [PATCH 21/45] explicitly state character encoding in crash reports --- app/dialog/crashhandler/crashhandler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/dialog/crashhandler/crashhandler.cpp b/app/dialog/crashhandler/crashhandler.cpp index 0c4ab384a..a09ebcc43 100644 --- a/app/dialog/crashhandler/crashhandler.cpp +++ b/app/dialog/crashhandler/crashhandler.cpp @@ -211,21 +211,21 @@ void CrashHandlerDialog::SendErrorReport() // Create description section QHttpPart desc_part; - desc_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain")); + desc_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain; charset=UTF-8")); desc_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"description\"")); desc_part.setBody(summary_edit_->toPlainText().toUtf8()); multipart->append(desc_part); // Create report section QHttpPart report_part; - report_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain")); + report_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain; charset=UTF-8")); report_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"report\"")); report_part.setBody(report_data_); multipart->append(report_part); // Create commit section QHttpPart commit_part; - commit_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain")); + commit_part.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/plain; charset=UTF-8")); commit_part.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"commit\"")); commit_part.setBody(GITHASH); multipart->append(commit_part); From b161ff710dc9e030905a694a9d82a89048fc66e6 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 10 Apr 2021 13:22:26 +1000 Subject: [PATCH 22/45] project: fix bug disconnecting folders --- app/node/project/projectviewmodel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/node/project/projectviewmodel.cpp b/app/node/project/projectviewmodel.cpp index 8c247aa23..08c0b773f 100644 --- a/app/node/project/projectviewmodel.cpp +++ b/app/node/project/projectviewmodel.cpp @@ -454,7 +454,7 @@ void ProjectViewModel::DisconnectItem(Node *n) disconnect(f, &Folder::BeginRemoveItem, this, &ProjectViewModel::ItemRemoved); foreach (Node* c, f->children()) { - ConnectItem(c); + DisconnectItem(c); } } } From 72669761a74c4c4c1ed3a10eb07f73759cbebbdd Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 10 Apr 2021 13:22:42 +1000 Subject: [PATCH 23/45] project: fix bug importing folders --- app/task/project/import/import.cpp | 23 ++++++++++++++--------- app/task/project/import/import.h | 2 ++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/task/project/import/import.cpp b/app/task/project/import/import.cpp index d88f1317a..3c317773b 100644 --- a/app/task/project/import/import.cpp +++ b/app/task/project/import/import.cpp @@ -96,13 +96,10 @@ void ProjectImportTask::Import(Folder *folder, QFileInfoList import, int &counte // Create a folder corresponding to the directory Folder* f = new Folder(); - f->moveToThread(folder->thread()); - f->SetLabel(file_info.fileName()); // Create undoable command that adds the items to the model - parent_command->add_child(new NodeAddCommand(folder->parent(), f)); - parent_command->add_child(new FolderAddChild(folder, f)); + AddItemToFolder(folder, f, parent_command); // Recursively follow this path Import(f, entry_list, counter, parent_command); @@ -119,11 +116,7 @@ void ProjectImportTask::Import(Folder *folder, QFileInfoList import, int &counte ValidateImageSequence(footage, import, i); // Create undoable command that adds the items to the model - NodeAddCommand* nac = new NodeAddCommand(folder->parent(), footage); - nac->PushToThread(folder->thread()); - parent_command->add_child(nac); - - parent_command->add_child(new FolderAddChild(folder, footage)); + AddItemToFolder(folder, footage, parent_command); } else { // Add to list so we can tell the user about it later invalid_files_.append(file_info.absoluteFilePath()); @@ -223,6 +216,18 @@ void ProjectImportTask::ValidateImageSequence(Footage *footage, QFileInfoList& i } } +void ProjectImportTask::AddItemToFolder(Folder *folder, Node *item, MultiUndoCommand *command) +{ + // Create undoable command that adds the items to the model + Project* project = model_->project(); + + NodeAddCommand* nac = new NodeAddCommand(project, item); + nac->PushToThread(project->thread()); + command->add_child(nac); + + command->add_child(new FolderAddChild(folder, item)); +} + bool ProjectImportTask::ItemIsStillImageFootageOnly(Footage* footage) { if (footage->GetTotalStreamCount() != 1) { diff --git a/app/task/project/import/import.h b/app/task/project/import/import.h index 6187d585a..7b1a3d6d9 100644 --- a/app/task/project/import/import.h +++ b/app/task/project/import/import.h @@ -61,6 +61,8 @@ private: void ValidateImageSequence(Footage *footage, QFileInfoList &info_list, int index); + void AddItemToFolder(Folder* folder, Node* item, MultiUndoCommand* command); + static bool ItemIsStillImageFootageOnly(Footage *footage); static bool CompareStillImageSize(Footage *footage, const QSize& sz); From 391cf81e345b1865d558694c01eef6ac0dcfd8a6 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 12:11:47 +1000 Subject: [PATCH 24/45] move 'first show' variable outside of ifdef --- app/window/mainwindow/mainwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 4cdf2e1d3..0cb3a4128 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -640,9 +640,9 @@ void MainWindow::showEvent(QShowEvent *e) if (!strcmp(vendor, "nouveau")) { QMetaObject::invokeMethod(this, "ShowNouveauWarning", Qt::QueuedConnection); } +#endif first_show_ = false; -#endif } } From ad59d09193bff6c11367e49b1014de00a6cff556 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 12:13:26 +1000 Subject: [PATCH 25/45] ci: extract symbols after deploy It's best to extract the symbols after any changes are made to the executable. --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index feb74cea3..3d16cedee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -351,13 +351,6 @@ jobs: $DOWNLOAD_TOOL https://github.com/arl/macdeployqtfix/raw/master/macdeployqtfix.py python2 macdeployqtfix.py $BUNDLE_NAME/Contents/MacOS/Olive $DEP_LOCATION - # Crashpad symbols - $DEP_LOCATION/bin/dump_syms $BUNDLE_NAME/Contents/MacOS/Olive > Olive.sym - SYM_HEADER=($(head -n 1 Olive.sym)) # Read first line of symbol file - SYM_DIR=$BUNDLE_NAME/Contents/Resources/symbols/Olive/${SYM_HEADER[3]} - mkdir -p "$SYM_DIR" - mv Olive.sym "$SYM_DIR" - # Manual fixes cp $DEP_LOCATION/lib/libopentimelineio.dylib $BUNDLE_NAME/Contents/Frameworks cp $DEP_LOCATION/lib/libopentime.dylib $BUNDLE_NAME/Contents/Frameworks @@ -367,6 +360,13 @@ jobs: install_name_tool -change libmodplug.dylib @rpath/libmodplug.dylib $BUNDLE_NAME/Contents/Frameworks/libavformat.* install_name_tool -change libmodplug.dylib @rpath/libmodplug.dylib $BUNDLE_NAME/Contents/Frameworks/libavfilter.* + # Crashpad symbols + $DEP_LOCATION/bin/dump_syms $BUNDLE_NAME/Contents/MacOS/Olive > Olive.sym + SYM_HEADER=($(head -n 1 Olive.sym)) # Read first line of symbol file + SYM_DIR=$BUNDLE_NAME/Contents/Resources/symbols/Olive/${SYM_HEADER[3]} + mkdir -p "$SYM_DIR" + mv Olive.sym "$SYM_DIR" + - name: Deploy Packages working-directory: ${{ runner.workspace }}/build shell: bash From 700b56d43f458c293182cd8be8f230f48293b7ee Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 12:20:50 +1000 Subject: [PATCH 26/45] node: limit bounding rect when detecting node collisions Hopefully prevents the possibility of an infinite loop. --- app/node/node.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/node/node.cpp b/app/node/node.cpp index 65dbe09f6..da1359e15 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -2279,7 +2279,7 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() commands_.append(set_pos_command); // Get bounding rect - QRectF bounding_rect(position_.x() - 0.5, position_.y() - 0.5, 1, 1); + QRectF bounding_rect(position_.x() - 0.45, position_.y() - 0.45, 0.9, 0.9); // Start moving other nodes foreach (Node* surrounding, node_->parent()->nodes()) { From bd052ff4c8f1648b2b7f629df2706e24e1052fe2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 12:26:58 +1000 Subject: [PATCH 27/45] timeline: null checks around any timestamp transform to audio timebase --- app/widget/timelinewidget/timelinewidget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 49f9e5d83..799b0e21c 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -1009,7 +1009,7 @@ void TimelineWidget::SetViewTimestamp(const int64_t &ts) for (int i=0;iview()->SetTime(Timecode::rescale_timestamp(ts, timebase(), GetConnectedNode()->GetAudioParams().sample_rate_as_time_base())); @@ -1021,7 +1021,7 @@ void TimelineWidget::SetViewTimestamp(const int64_t &ts) void TimelineWidget::ViewTimestampChanged(int64_t ts) { - if (use_audio_time_units_ && sender() == views_.at(Track::kAudio)) { + if (GetConnectedNode() && use_audio_time_units_ && sender() == views_.at(Track::kAudio)) { ts = Timecode::rescale_timestamp(ts, GetConnectedNode()->GetAudioParams().sample_rate_as_time_base(), timebase()); From a54b42fea42df5c5b9a75d500796b52cfb3a7a38 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 12:52:46 +1000 Subject: [PATCH 28/45] nodeview: attach nodes to cursor as an undo command --- app/widget/nodeview/nodeview.cpp | 34 +++++++++++++++++++++++++++----- app/widget/nodeview/nodeview.h | 18 +++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 348b7a9cd..94028868b 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -298,11 +298,11 @@ void NodeView::Paste() QVector pasted_nodes = PasteNodesFromClipboard(graph_, command); - Core::instance()->undo_stack()->pushIfHasChildren(command); - if (!pasted_nodes.isEmpty()) { - AttachNodesToCursor(pasted_nodes); + command->add_child(new NodeViewAttachNodesToCursor(this, pasted_nodes)); } + + Core::instance()->undo_stack()->pushIfHasChildren(command); } void NodeView::Duplicate() @@ -321,9 +321,11 @@ void NodeView::Duplicate() QVector duplicated_nodes = Node::CopyDependencyGraph(selected, command); - Core::instance()->undo_stack()->pushIfHasChildren(command); + if (!duplicated_nodes.isEmpty()) { + command->add_child(new NodeViewAttachNodesToCursor(this, duplicated_nodes)); + } - AttachNodesToCursor(duplicated_nodes); + Core::instance()->undo_stack()->pushIfHasChildren(command); } void NodeView::SetColorLabel(int index) @@ -839,4 +841,26 @@ void NodeView::ZoomFromKeyboard(double multiplier) ZoomIntoCursorPosition(multiplier, cursor_pos); } +NodeView::NodeViewAttachNodesToCursor::NodeViewAttachNodesToCursor(NodeView *view, const QVector &nodes) : + view_(view), + nodes_(nodes) +{ +} + +void NodeView::NodeViewAttachNodesToCursor::redo() +{ + view_->AttachNodesToCursor(nodes_); +} + +void NodeView::NodeViewAttachNodesToCursor::undo() +{ + view_->DetachItemsFromCursor(); +} + +Project *NodeView::NodeViewAttachNodesToCursor::GetRelevantProject() const +{ + // Will either return a project or a nullptr which is also acceptable + return dynamic_cast(view_->graph_); +} + } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 67e13cc8c..792d697b2 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -110,6 +110,24 @@ private: void ZoomFromKeyboard(double multiplier); + class NodeViewAttachNodesToCursor : public UndoCommand + { + public: + NodeViewAttachNodesToCursor(NodeView* view, const QVector& nodes); + + virtual void redo() override; + + virtual void undo() override; + + virtual Project * GetRelevantProject() const override; + + private: + NodeView* view_; + + QVector nodes_; + + }; + NodeGraph* graph_; struct AttachedItem { From e89e348f6e98ff774e586c9e00c3725cb05150e3 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 12:59:25 +1000 Subject: [PATCH 29/45] nodegraph: delete nodes bottom to top when clearing --- app/node/graph.cpp | 9 ++++++++- app/node/graph.h | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/node/graph.cpp b/app/node/graph.cpp index 52823d69d..f76f390b4 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -30,10 +30,17 @@ NodeGraph::NodeGraph() { } +NodeGraph::~NodeGraph() +{ + Clear(); +} + void NodeGraph::Clear() { + // By deleting the last nodes first, we assume that nodes that are most important are deleted last + // (e.g. Project's ColorManager or ProjectSettingsNode. while (!node_children_.isEmpty()) { - delete node_children_.first(); + delete node_children_.last(); } } diff --git a/app/node/graph.h b/app/node/graph.h index bb65bdb42..a49f21fcf 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -40,6 +40,11 @@ public: */ NodeGraph(); + /** + * @brief NodeGraph Destructor + */ + virtual ~NodeGraph() override; + /** * @brief Destructively destroys all nodes in the graph */ From f9b60538fcd54ad74b334514defc718ef33fe6e0 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 18:52:12 +1000 Subject: [PATCH 30/45] project: removed footage comparison code that had been stubbed anyway --- app/core.cpp | 4 +- .../footagerelink/footagerelinkdialog.cpp | 16 +-- app/node/project/footage/footage.cpp | 104 ------------------ app/node/project/footage/footage.h | 3 - 4 files changed, 10 insertions(+), 117 deletions(-) diff --git a/app/core.cpp b/app/core.cpp index 92df5c3e7..6b0cebd5c 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -1504,8 +1504,8 @@ bool Core::ValidateFootageInLoadedProject(Project* project, const QString& proje } } - // Heuristically compare footage to file - if (Footage::CompareFootageToItsFilename(footage)) { + if (QFileInfo::exists(footage->filename())) { + // Assume valid footage->SetValid(); } else { footage_we_couldnt_validate.append(footage); diff --git a/app/dialog/footagerelink/footagerelinkdialog.cpp b/app/dialog/footagerelink/footagerelinkdialog.cpp index 7c3cf9c4e..be04e097b 100644 --- a/app/dialog/footagerelink/footagerelinkdialog.cpp +++ b/app/dialog/footagerelink/footagerelinkdialog.cpp @@ -114,13 +114,14 @@ void FootageRelinkDialog::BrowseForFootage() // Set new filename since this was set manually by the user f->set_filename(new_fn); - if (Footage::CompareFootageToItsFilename(f)) { - // Set footage to valid and update icon - f->SetValid(); + // Assume footage is valid here. We could do some decoder probing to ensure it's a usable file + // but otherwise we assume the user knows what they're doing here. - // Update item visually - UpdateFootageItem(index); - } + // Set footage to valid and update icon + f->SetValid(); + + // Update item visually + UpdateFootageItem(index); // Check all other footage files for matches for (int it=0; itset_filename(absolute_to_new); other_footage->SetValid(); UpdateFootageItem(it); diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 1642d1e73..83e9e4eb4 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -298,41 +298,6 @@ QString Footage::DescribeAudioStream(const AudioParams ¶ms) QString::number(params.sample_rate())); } -bool Footage::CompareFootageToFile(Footage *footage, const QString &filename) -{ - // Heuristic to determine if file has changed - QFileInfo info(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. - DecoderPtr decoder = Decoder::CreateFromID(footage->decoder()); - - Streams probed_streams = decoder->Probe(filename, nullptr); - - if (probed_streams == footage->streams_) { - return true; - } - }*/ - Q_UNUSED(footage) - - // Simplified, since our footage node is much more tolerant, we'll try this - return true; - } - - // Footage file couldn't be found or resolved to something we didn't expect - return false; -} - -bool Footage::CompareFootageToItsFilename(Footage *footage) -{ - return CompareFootageToFile(footage, footage->filename()); -} - void Footage::Hash(const QString& output, QCryptographicHash &hash, const rational &time) const { super::Hash(output, hash, time); @@ -490,58 +455,6 @@ void Footage::UpdateTooltip() } } -/*void Footage::AddStreamAsInput(Track::Type type, int index, QVariant value) -{ - QString input_id = GetInputIDOfIndex(type, index); - - Track::Reference ref(type, index); - - // Create input for parameters - NodeValue::Type value_type; - uint64_t param_mask = 0; - - if (type == Track::kVideo) { - VideoParams vp = value.value(); - value_type = NodeValue::kVideoParams; - - // Universal parameters for video/image footage - param_mask |= VideoParamEdit::kEnabled; - param_mask |= VideoParamEdit::kColorspace; - param_mask |= VideoParamEdit::kPixelAspect; - param_mask |= VideoParamEdit::kInterlacing; - - if (vp.channel_count() == VideoParams::kRGBAChannelCount) { - // If this has an alpha channel, add a premultiplied optino - param_mask |= VideoParamEdit::kPremultipliedAlpha; - } - - if (vp.video_type() != VideoParams::kVideoTypeVideo) { - // This is either a still image or an image sequence, add properties for those - param_mask |= VideoParamEdit::kIsImageSequence; - param_mask |= VideoParamEdit::kStartTime; - param_mask |= VideoParamEdit::kEndTime; - param_mask |= VideoParamEdit::kFrameRate; - } else { - // Ensure timebase isn't overwritten by the frame rate field - param_mask |= VideoParamEdit::kFrameRateIsNotTimebase; - } - } else { - value_type = NodeValue::kAudioParams; - param_mask = 0; - } - - AddInput(input_id, value_type, - InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); - SetStandardValue(input_id, value); - SetInputProperty(input_id, QStringLiteral("mask"), QVariant::fromValue(param_mask)); - inputs_for_stream_properties_.insert(ref, input_id); - - // Create output for stream - QString output_id = Track::Reference(type, index).ToString(); - AddOutput(output_id); - outputs_for_streams_.insert(ref, output_id); -}*/ - void Footage::CheckFootage() { QString fn = filename(); @@ -559,21 +472,4 @@ void Footage::CheckFootage() } } -/*QString Track::Reference::video_colorspace(bool default_if_empty) const -{ - if (IsValid()) { - VideoParams params = footage_->GetVideoParams(index_); - - if (params.is_valid()) { - if (params.colorspace().isEmpty() && default_if_empty) { - - } else { - return params.colorspace(); - } - } - } - - return QString(); -}*/ - } diff --git a/app/node/project/footage/footage.h b/app/node/project/footage/footage.h index 96336a737..153880987 100644 --- a/app/node/project/footage/footage.h +++ b/app/node/project/footage/footage.h @@ -170,9 +170,6 @@ public: static QString DescribeVideoStream(const VideoParams& params); static QString DescribeAudioStream(const AudioParams& params); - static bool CompareFootageToFile(Footage* footage, const QString& filename); - static bool CompareFootageToItsFilename(Footage* footage); - virtual void Hash(const QString& output, QCryptographicHash &hash, const rational &time) const override; virtual NodeValueTable Value(const QString &output, NodeValueDatabase& value) const override; From 210eb86cd2c2ceab868179ebcfac8637419b2a87 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 18:54:22 +1000 Subject: [PATCH 31/45] nodes: fixed hashing issues --- app/node/block/clip/clip.cpp | 7 +++++-- app/node/math/merge/merge.cpp | 8 ++++++-- app/node/output/track/track.cpp | 4 +++- app/node/project/footage/footage.cpp | 21 +++++++++------------ app/render/previewautocacher.cpp | 2 +- app/render/rendermanager.cpp | 4 ++-- app/render/rendermanager.h | 6 +++++- app/task/render/render.cpp | 2 +- 8 files changed, 32 insertions(+), 22 deletions(-) diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index e7b7f9799..ba2c93d0c 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -114,12 +114,15 @@ void ClipBlock::Retranslate() SetInputName(kBufferIn, tr("Buffer")); } -void ClipBlock::Hash(const QString &output, QCryptographicHash &hash, const rational &time) const +void ClipBlock::Hash(const QString &out, QCryptographicHash &hash, const rational &time) const { + Q_UNUSED(out) + if (IsInputConnected(kBufferIn)) { rational t = InputTimeAdjustment(kBufferIn, -1, TimeRange(time, time)).in(); - GetConnectedNode(kBufferIn)->Hash(output, hash, t); + NodeOutput output = GetConnectedOutput(kBufferIn); + output.node()->Hash(output.output(), hash, t); } } diff --git a/app/node/math/merge/merge.cpp b/app/node/math/merge/merge.cpp index 823a101b2..0051e95d5 100644 --- a/app/node/math/merge/merge.cpp +++ b/app/node/math/merge/merge.cpp @@ -107,13 +107,16 @@ void MergeNode::Hash(const QString &output, QCryptographicHash &hash, const rati // connected node happens to return nothing (a gap for instance). Therefore we only add our // fingerprint if the base AND the blend change the hash. Otherwise, we assume it's a passthrough. + Q_UNUSED(output) + QByteArray current_result = hash.result(); bool base_changed_hash = false; bool blend_changed_hash = false; if (IsInputConnected(kBaseIn)) { - GetConnectedNode(kBaseIn)->Hash(output, hash, time); + NodeOutput base_output = GetConnectedOutput(kBaseIn); + base_output.node()->Hash(base_output.output(), hash, time); QByteArray post_base_hash = hash.result(); base_changed_hash = (post_base_hash != current_result); @@ -121,7 +124,8 @@ void MergeNode::Hash(const QString &output, QCryptographicHash &hash, const rati } if(IsInputConnected(kBlendIn)) { - GetConnectedNode(kBlendIn)->Hash(output, hash, time); + NodeOutput blend_output = GetConnectedOutput(kBlendIn); + blend_output.node()->Hash(blend_output.output(), hash, time); blend_changed_hash = (hash.result() != current_result); } diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 3740d311d..5dd552e58 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -574,11 +574,13 @@ bool Track::IsLocked() const void Track::Hash(const QString &output, QCryptographicHash &hash, const rational &time) const { + Q_UNUSED(output) + Block* b = BlockAtTime(time); // Defer to block at this time, don't add any of our own information to the hash if (b) { - b->Hash(output, hash, TransformTimeForBlock(b, time)); + b->Hash(kDefaultOutput, hash, TransformTimeForBlock(b, time)); } } diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 83e9e4eb4..96122ad45 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -302,27 +302,23 @@ void Footage::Hash(const QString& output, QCryptographicHash &hash, const ration { super::Hash(output, hash, time); + // Footage last modified date + hash.addData(QString::number(timestamp()).toUtf8()); + // Translate output ID to stream Track::Reference ref = Track::Reference::FromString(output); - QString fn = filename(); - - if (!fn.isEmpty()) { + if (ref.type() == Track::kVideo) { VideoParams params = GetVideoParams(ref.index()); if (params.is_valid()) { // Add footage details to hash - - // Footage filename - hash.addData(filename().toUtf8()); - - // Footage last modified date - hash.addData(QString::number(timestamp()).toUtf8()); + QString fn = filename(); // Footage stream hash.addData(QString::number(ref.index()).toUtf8()); - if (ref.type() == Track::kVideo) { + if (!fn.isEmpty()) { // Current color config and space hash.addData(project()->color_manager()->GetConfigFilename().toUtf8()); hash.addData(GetColorspaceToUse(params).toUtf8()); @@ -338,10 +334,11 @@ void Footage::Hash(const QString& output, QCryptographicHash &hash, const ration int64_t video_ts = Timecode::time_to_timestamp(time, params.time_base()); // Add timestamp in units of the video stream's timebase - hash.addData(reinterpret_cast(&video_ts), sizeof(int64_t)); + hash.addData(reinterpret_cast(&video_ts), sizeof(video_ts)); // Add start time - used for both image sequences and video streams - hash.addData(QString::number(params.start_time()).toUtf8()); + auto start_time = params.start_time(); + hash.addData(reinterpret_cast(&start_time), sizeof(start_time)); } } } diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index ed681955b..12eabb035 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -69,7 +69,7 @@ void PreviewAutoCacher::GenerateHashes(ViewerOutput *viewer, FrameHashCache* cac foreach (const rational& time, times) { // See if hash already exists in disk cache - QByteArray hash = RenderManager::Hash(viewer->GetConnectedNode(ViewerOutput::kTextureInput), viewer->GetVideoParams(), time); + QByteArray hash = RenderManager::Hash(viewer->GetConnectedTextureOutput(), viewer->GetVideoParams(), time); // Check memory list since disk checking is slow bool hash_exists = (std::find(existing_hashes.begin(), existing_hashes.end(), hash) != existing_hashes.end()); diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index 73449d9ea..0eac197fe 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -79,7 +79,7 @@ RenderManager::~RenderManager() } } -QByteArray RenderManager::Hash(const Node *n, const VideoParams ¶ms, const rational &time) +QByteArray RenderManager::Hash(const Node *n, const QString& output, const VideoParams ¶ms, const rational &time) { QCryptographicHash hasher(QCryptographicHash::Sha1); @@ -93,7 +93,7 @@ QByteArray RenderManager::Hash(const Node *n, const VideoParams ¶ms, const r hasher.addData(reinterpret_cast(&format), sizeof(VideoParams::Format)); if (n) { - n->Hash(Node::kDefaultOutput, hasher, time); + n->Hash(output, hasher, time); } return hasher.result(); diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h index 7881894a0..df68fe5ca 100644 --- a/app/render/rendermanager.h +++ b/app/render/rendermanager.h @@ -67,7 +67,11 @@ public: /** * @brief Generate a unique identifier for a certain node at a certain time */ - static QByteArray Hash(const Node *n, const VideoParams ¶ms, const rational &time); + static QByteArray Hash(const Node *n, const QString &output, const VideoParams ¶ms, const rational &time); + static QByteArray Hash(const NodeOutput &output, const VideoParams ¶ms, const rational &time) + { + return Hash(output.node(), output.output(), params, time); + } /** * @brief Asynchronously generate a frame at a given time diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 0511f808c..009d5bee1 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -85,7 +85,7 @@ bool RenderTask::Render(ColorManager* manager, return true; } - hashes[i] = RenderManager::instance()->Hash(viewer(), video_params_, times.at(i)); + hashes[i] = RenderManager::instance()->Hash(viewer()->GetConnectedTextureOutput(), video_params_, times.at(i)); } // Filter out duplicates From 6c30f8cf46497a1780d038205d52cb9aa0773ecf Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 18:56:39 +1000 Subject: [PATCH 32/45] nodes: implemented infrastructure for caching any node Not accessible through UI yet, but ported Footage node to it (previously we used a hack to ensure Footage textures were cached, now it's a formal infrastructure). --- app/node/node.cpp | 3 +- app/node/node.h | 12 ++++++ app/node/project/footage/footage.cpp | 2 + app/node/traverser.cpp | 37 +++++++++++----- app/node/traverser.h | 16 ++++++- app/render/framehashcache.cpp | 20 ++++++--- app/render/framehashcache.h | 4 +- app/render/rendermanager.cpp | 2 +- app/render/renderprocessor.cpp | 64 ++++++++++++++++++++-------- app/render/renderprocessor.h | 8 +++- 10 files changed, 129 insertions(+), 39 deletions(-) diff --git a/app/node/node.cpp b/app/node/node.cpp index da1359e15..b2b02d807 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -48,7 +48,8 @@ Node::Node(bool create_default_output) : override_color_(-1), last_change_time_(0), folder_(nullptr), - operation_stack_(0) + operation_stack_(0), + cache_result_(false) { if (create_default_output) { AddOutput(); diff --git a/app/node/node.h b/app/node/node.h index fd3bb626f..86e4d2f54 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -756,6 +756,16 @@ public: folder_ = folder; } + bool GetCacheTextures() const + { + return cache_result_; + } + + void SetCacheTextures(bool e) + { + cache_result_ = e; + } + static const QString kDefaultOutput; protected: @@ -1191,6 +1201,8 @@ private: int operation_stack_; + bool cache_result_; + private slots: /** * @brief Slot when a keyframe's time changes to keep the keyframes correctly sorted by time diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 96122ad45..796e96cfe 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -48,6 +48,8 @@ Footage::Footage(const QString &filename) : Clear(); set_filename(filename); + + SetCacheTextures(true); } void Footage::Retranslate() diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index 0a5232867..f7d0eb099 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -22,6 +22,7 @@ #include "node.h" #include "render/job/footagejob.h" +#include "render/rendermanager.h" namespace olive { @@ -110,7 +111,7 @@ NodeValueTable NodeTraverser::GenerateTable(const Node *n, const QString& output // By this point, the node should have all the inputs it needs to render correctly NodeValueTable table = n->Value(output, database); - PostProcessTable(n, range, table); + PostProcessTable(n, output, range, table); return table; } @@ -171,10 +172,15 @@ QVariant NodeTraverser::ProcessFrameGeneration(const Node *node, const GenerateJ return QVariant(); } -QVariant NodeTraverser::GetCachedFrame(const Node *node, const rational &time) +void NodeTraverser::SaveCachedTexture(const QByteArray &hash, const QVariant &texture) { - Q_UNUSED(node) - Q_UNUSED(time) + Q_UNUSED(hash) + Q_UNUSED(texture) +} + +QVariant NodeTraverser::GetCachedTexture(const QByteArray& hash) +{ + Q_UNUSED(hash) return QVariant(); } @@ -190,17 +196,23 @@ void NodeTraverser::AddGlobalsToDatabase(NodeValueDatabase &db, const TimeRange& db.Insert(QStringLiteral("global"), global); } -void NodeTraverser::PostProcessTable(const Node *node, const TimeRange &range, NodeValueTable &output_params) +void NodeTraverser::PostProcessTable(const Node *node, const QString& output, const TimeRange &range, NodeValueTable &output_params) { bool got_cached_frame = false; + QByteArray cached_node_hash; // Convert footage to image/sample buffers - QVariant cached_frame = GetCachedFrame(node, range.in()); - if (!cached_frame.isNull()) { - output_params.Push(NodeValue::kTexture, cached_frame, node); + if (CanCacheFrames() && node->GetCacheTextures()) { + // This node is set to cache the result, see if we can retrieved a previously cached version + cached_node_hash = RenderManager::Hash(node, output, GetCacheVideoParams(), range.in()); - // No more to do here - got_cached_frame = true; + QVariant cached_frame = GetCachedTexture(cached_node_hash); + if (!cached_frame.isNull()) { + output_params.Push(NodeValue::kTexture, cached_frame, node); + + // No more to do here + got_cached_frame = true; + } } // Strip out any jobs or footage @@ -285,6 +297,11 @@ void NodeTraverser::PostProcessTable(const Node *node, const TimeRange &range, N output_params.Push(NodeValue::kSamples, value, node); } } + + if (CanCacheFrames() && node->GetCacheTextures() && !got_cached_frame) { + // Save cached texture + SaveCachedTexture(cached_node_hash, output_params.Get(NodeValue::kTexture)); + } } } diff --git a/app/node/traverser.h b/app/node/traverser.h index cb0da8f60..d62c76581 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -59,7 +59,19 @@ protected: virtual QVariant ProcessFrameGeneration(const Node *node, const GenerateJob& job); - virtual QVariant GetCachedFrame(const Node *node, const rational &time); + virtual QVariant GetCachedTexture(const QByteArray& hash); + + virtual void SaveCachedTexture(const QByteArray& hash, const QVariant& texture); + + virtual bool CanCacheFrames() + { + return false; + } + + virtual VideoParams GetCacheVideoParams() + { + return VideoParams(); + } void AddGlobalsToDatabase(NodeValueDatabase& db, const TimeRange &range) const; @@ -69,7 +81,7 @@ protected: } private: - void PostProcessTable(const Node *node, const TimeRange &range, NodeValueTable &output_params); + void PostProcessTable(const Node *node, const QString &output, const TimeRange &range, NodeValueTable &output_params); }; diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index f0d56b79f..a7644d636 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -187,14 +187,24 @@ bool FrameHashCache::SaveCacheFrame(const QByteArray& hash, const VideoParams& vparam, int linesize_bytes) const { - QString fn = CachePathName(hash); + return SaveCacheFrame(GetCacheDirectory(), hash, data, vparam, linesize_bytes); +} + +bool FrameHashCache::SaveCacheFrame(const QByteArray &hash, FramePtr frame) const +{ + return SaveCacheFrame(GetCacheDirectory(), hash, frame); +} + +bool FrameHashCache::SaveCacheFrame(const QString &cache_path, const QByteArray &hash, char *data, const VideoParams &vparam, int linesize_bytes) +{ + QString fn = CachePathName(cache_path, hash); if (SaveCacheFrame(fn, data, vparam, linesize_bytes)) { // Register frame with the disk manager QMetaObject::invokeMethod(DiskManager::instance(), "CreatedFile", Qt::QueuedConnection, - Q_ARG(QString, GetCacheDirectory()), + Q_ARG(QString, cache_path), Q_ARG(QString, fn), Q_ARG(QByteArray, hash)); @@ -204,10 +214,10 @@ bool FrameHashCache::SaveCacheFrame(const QByteArray& hash, } } -bool FrameHashCache::SaveCacheFrame(const QByteArray &hash, FramePtr frame) const +bool FrameHashCache::SaveCacheFrame(const QString &cache_path, const QByteArray &hash, FramePtr frame) { if (frame) { - return SaveCacheFrame(hash, frame->data(), frame->video_params(), frame->linesize_bytes()); + return SaveCacheFrame(cache_path, hash, frame->data(), frame->video_params(), frame->linesize_bytes()); } else { qWarning() << "Attempted to save a NULL frame to the cache. This may or may not be desirable."; return false; @@ -394,7 +404,7 @@ QString FrameHashCache::CachePathName(const QString &cache_path, const QByteArra return cache_dir.filePath(filename); } -bool FrameHashCache::SaveCacheFrame(const QString &filename, char *data, const VideoParams &vparam, int linesize_bytes) const +bool FrameHashCache::SaveCacheFrame(const QString &filename, char *data, const VideoParams &vparam, int linesize_bytes) { if (!VideoParams::FormatIsFloat(vparam.format())) { qCritical() << "Tried to cache frame with non-float pixel format"; diff --git a/app/render/framehashcache.h b/app/render/framehashcache.h index 3ff6eb9b7..1b4d64748 100644 --- a/app/render/framehashcache.h +++ b/app/render/framehashcache.h @@ -61,9 +61,11 @@ public: QString CachePathName(const QByteArray &hash) const; static QString CachePathName(const QString& cache_path, const QByteArray &hash); - bool SaveCacheFrame(const QString& filename, char *data, const VideoParams &vparam, int linesize_bytes) const; + static bool SaveCacheFrame(const QString& filename, char *data, const VideoParams &vparam, int linesize_bytes); bool SaveCacheFrame(const QByteArray& hash, char *data, const VideoParams &vparam, int linesize_bytes) const; bool SaveCacheFrame(const QByteArray& hash, FramePtr frame) const; + static bool SaveCacheFrame(const QString& cache_path, const QByteArray& hash, char *data, const VideoParams &vparam, int linesize_bytes); + static bool SaveCacheFrame(const QString& cache_path, const QByteArray& hash, FramePtr frame); static FramePtr LoadCacheFrame(const QString& cache_path, const QByteArray& hash); FramePtr LoadCacheFrame(const QByteArray& hash) const; static FramePtr LoadCacheFrame(const QString& fn); diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index 0eac197fe..3ea8c5102 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -189,7 +189,7 @@ RenderTicketPtr RenderManager::SaveFrameToCache(FrameHashCache *cache, FramePtr // Create ticket RenderTicketPtr ticket = std::make_shared(); - ticket->setProperty("cache", Node::PtrToValue(cache)); + ticket->setProperty("cache", cache->GetCacheDirectory()); ticket->setProperty("frame", QVariant::fromValue(frame)); ticket->setProperty("hash", hash); ticket->setProperty("type", kTypeVideoDownload); diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 948e78fe0..63bad549c 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -148,11 +148,11 @@ void RenderProcessor::Run() } case RenderManager::kTypeVideoDownload: { - FrameHashCache* cache = Node::ValueToPtr(ticket_->property("cache")); + QString cache = ticket_->property("cache").toString(); FramePtr frame = ticket_->property("frame").value(); QByteArray hash = ticket_->property("hash").toByteArray(); - ticket_->Finish(cache->SaveCacheFrame(hash, frame), false); + ticket_->Finish(FrameHashCache::SaveCacheFrame(cache, hash, frame), false); break; } default: @@ -534,34 +534,62 @@ QVariant RenderProcessor::ProcessFrameGeneration(const Node *node, const Generat return QVariant::fromValue(texture); } -QVariant RenderProcessor::GetCachedFrame(const Node *node, const rational &time) +bool RenderProcessor::CanCacheFrames() { - if (!ticket_->property("cache").toString().isEmpty() - && node->id() == QStringLiteral("org.olivevideoeditor.Olive.videoinput")) { - const VideoParams& video_params = ticket_->property("vparam").value(); + return true; +} - QByteArray hash = RenderManager::Hash(node, video_params, time); +QVariant RenderProcessor::GetCachedTexture(const QByteArray& hash) +{ + VideoParams video_params = GetCacheVideoParams(); + QString cache_dir = ticket_->property("cache").toString(); - FramePtr f = FrameHashCache::LoadCacheFrame(ticket_->property("cache").toString(), hash); + FramePtr f = FrameHashCache::LoadCacheFrame(cache_dir, hash); - if (f) { - // The cached frame won't load with the correct divider by default, so we enforce it here - VideoParams p = f->video_params(); + if (f) { + // The cached frame won't load with the correct divider by default, so we enforce it here + VideoParams p = f->video_params(); - p.set_width(f->width() * video_params.divider()); - p.set_height(f->height() * video_params.divider()); - p.set_divider(video_params.divider()); + p.set_width(f->width() * video_params.divider()); + p.set_height(f->height() * video_params.divider()); + p.set_divider(video_params.divider()); - f->set_video_params(p); + f->set_video_params(p); - TexturePtr texture = render_ctx_->CreateTexture(f->video_params(), f->data(), f->linesize_pixels()); - return QVariant::fromValue(texture); - } + TexturePtr texture = render_ctx_->CreateTexture(f->video_params(), f->data(), f->linesize_pixels()); + qDebug() << "Loaded mid-render frame from cache"; + return QVariant::fromValue(texture); } return QVariant(); } +void RenderProcessor::SaveCachedTexture(const QByteArray &hash, const QVariant &tex_var) +{ + // FIXME: Temporarily disabled because I don't know how to ensure that the frame saved here is + // not the main frame. If it is, it'll be saved twice which will waste a lot of cycles. + // At least disabled, the frame will still save, and if nothing else alters the hash, it + // will pick up automatically from GetCachedTexture. + /*if (!tex_var.isNull()) { + QString cache_dir = ticket_->property("cache").toString(); + + if (!cache_dir.isEmpty()) { + TexturePtr texture = tex_var.value(); + FramePtr frame = Frame::Create(); + frame->set_video_params(texture->params()); + frame->allocate(); + render_ctx_->DownloadFromTexture(texture.get(), frame->data(), frame->linesize_pixels()); + FrameHashCache::SaveCacheFrame(cache_dir, hash, frame); + qDebug() << "Saved mid-render frame to cache"; + } + }*/ +} + +VideoParams RenderProcessor::GetCacheVideoParams() +{ + return ticket_->property("vparam").value(); +} + QVector2D RenderProcessor::GenerateResolution() const { // Set resolution to the destination to the "logical" resolution of the destination diff --git a/app/render/renderprocessor.h b/app/render/renderprocessor.h index 0e5754b02..469a007ed 100644 --- a/app/render/renderprocessor.h +++ b/app/render/renderprocessor.h @@ -53,7 +53,13 @@ protected: virtual QVariant ProcessFrameGeneration(const Node *node, const GenerateJob& job) override; - virtual QVariant GetCachedFrame(const Node *node, const rational &time) override; + virtual bool CanCacheFrames() override; + + virtual QVariant GetCachedTexture(const QByteArray &hash) override; + + virtual void SaveCachedTexture(const QByteArray& hash, const QVariant& texture) override; + + virtual VideoParams GetCacheVideoParams() override; virtual QVector2D GenerateResolution() const override; From a87adbd801fd0089cdf8b38f01bca200ba6c1a67 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 18:57:21 +1000 Subject: [PATCH 33/45] fixed precache task Fixes #1558 --- app/node/project/project.cpp | 10 ---------- app/node/project/project.h | 5 +++-- app/task/precache/precachetask.cpp | 20 +++++++++++++------ app/task/precache/precachetask.h | 4 ++-- .../projectexplorer/projectexplorer.cpp | 18 ++++++++++------- 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/app/node/project/project.cpp b/app/node/project/project.cpp index d16c78d94..faae9c903 100644 --- a/app/node/project/project.cpp +++ b/app/node/project/project.cpp @@ -220,16 +220,6 @@ void Project::set_filename(const QString &s) emit NameChanged(); } -ColorManager *Project::color_manager() -{ - return color_manager_; -} - -bool Project::is_modified() const -{ - return is_modified_; -} - void Project::set_modified(bool e) { is_modified_ = e; diff --git a/app/node/project/project.h b/app/node/project/project.h index 415307569..5d1471d6f 100644 --- a/app/node/project/project.h +++ b/app/node/project/project.h @@ -62,9 +62,10 @@ public: QString pretty_filename() const; void set_filename(const QString& s); - ColorManager* color_manager(); + ColorManager* color_manager() { return color_manager_; } + ProjectSettingsNode* settings() { return settings_; } - bool is_modified() const; + bool is_modified() const { return is_modified_; } void set_modified(bool e); bool has_autorecovery_been_saved() const; diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp index 28d08954e..703298889 100644 --- a/app/task/precache/precachetask.cpp +++ b/app/task/precache/precachetask.cpp @@ -27,23 +27,31 @@ namespace olive { PreCacheTask::PreCacheTask(Footage *footage, int index, Sequence* sequence) : RenderTask(new ViewerOutput(), sequence->GetVideoParams(), sequence->GetAudioParams()) { + // Create new project + project_ = new Project(); + + // Create viewer with same parameters as the sequence + viewer()->setParent(project_); viewer()->SetVideoParams(sequence->GetVideoParams()); viewer()->SetAudioParams(sequence->GetAudioParams()); + // Copy project config nodes + Node::CopyInputs(footage->project()->color_manager(), project_->color_manager(), false); + Node::CopyInputs(footage->project()->settings(), project_->settings(), false); + // Copy footage node so it can precache without any modifications from the user screwing it up footage_ = static_cast(footage->copy()); - index_ = index; + footage_->setParent(project_); Node::CopyInputs(footage, footage_, false); + Node::ConnectEdge(NodeOutput(footage_, Track::Reference(Track::kVideo, index).ToString()), NodeInput(viewer(), ViewerOutput::kTextureInput)); - Node::ConnectEdge(NodeOutput(footage_, Track::Reference(Track::kVideo, 0).ToString()), NodeInput(viewer(), ViewerOutput::kTextureInput)); - - SetTitle(tr("Pre-caching %1:%2").arg(footage_->filename())); + SetTitle(tr("Pre-caching %1:%2").arg(footage_->filename(), index)); } PreCacheTask::~PreCacheTask() { - // We created this viewer node ourselves, so now we should delete it - delete viewer(); + // This should delete the footage we copied and the viewer we created + delete project_; } bool PreCacheTask::Run() diff --git a/app/task/precache/precachetask.h b/app/task/precache/precachetask.h index bcf1358e9..96d31d1a7 100644 --- a/app/task/precache/precachetask.h +++ b/app/task/precache/precachetask.h @@ -43,9 +43,9 @@ protected: virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; private: - Footage* footage_; + Project* project_; - int index_; + Footage* footage_; }; diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index c5f23b965..3c870caba 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -490,15 +490,19 @@ void ProjectExplorer::ContextMenuStartProxy(QAction *a) Sequence* sequence = Node::ValueToPtr(a->data()); // To get here, the `context_menu_items_` must be all kFootage - foreach (Node* i, context_menu_items_) { - Footage* f = static_cast(i); + foreach (Node* item, context_menu_items_) { + Footage* f = static_cast(item); - QVector enabled_streams = f->GetEnabledVideoStreams(); + int sz = f->InputArraySize(Footage::kVideoParamsInput); - foreach (const VideoParams& stream, enabled_streams) { - // Start a background task for proxying - PreCacheTask* proxy_task = new PreCacheTask(f, stream.stream_index(), sequence); - TaskManager::instance()->AddTask(proxy_task); + for (int j=0; jGetVideoParams(j); + + if (vp.enabled()) { + // Start a background task for proxying + PreCacheTask* proxy_task = new PreCacheTask(f, j, sequence); + TaskManager::instance()->AddTask(proxy_task); + } } } } From afedea7342f9ebf3eaa8c4a726dc46a1a9f8e9d9 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 18:57:42 +1000 Subject: [PATCH 34/45] restore old node positioning code New code didn't work. --- app/node/node.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/node/node.cpp b/app/node/node.cpp index b2b02d807..1f4f4326b 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -2280,18 +2280,21 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() commands_.append(set_pos_command); // Get bounding rect - QRectF bounding_rect(position_.x() - 0.45, position_.y() - 0.45, 0.9, 0.9); + QRectF bounding_rect(position_.x() - 0.5, position_.y() - 0.5, 1, 1); // Start moving other nodes foreach (Node* surrounding, node_->parent()->nodes()) { if (bounding_rect.contains(surrounding->GetPosition()) && surrounding != node_) { QPointF new_pos = surrounding->GetPosition(); - if (surrounding->GetPosition().y() > position_.y()) { - new_pos.setY(new_pos.y() + 0.5); - } else { - new_pos.setY(new_pos.y() - 0.5); + + qreal move_rate = 0.50; + + if (surrounding->GetPosition().y() < position_.y()) { + move_rate = -move_rate; } + new_pos.setY(new_pos.y() + move_rate); + auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, new_pos, true); sur_command->redo(); commands_.append(sur_command); From 515d459d363d91aceb5c5129eb81f12ccbdeec60 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 19:38:29 +1000 Subject: [PATCH 35/45] nodeparamview: make keyframes on every key track by default Fixes #1437 --- app/node/inputdragger.cpp | 23 +++++++++++++---- app/node/inputdragger.h | 5 ++-- .../nodeparamviewwidgetbridge.cpp | 25 +++++++++++++------ 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/app/node/inputdragger.cpp b/app/node/inputdragger.cpp index 0d835fc77..ebf00874d 100644 --- a/app/node/inputdragger.cpp +++ b/app/node/inputdragger.cpp @@ -36,7 +36,7 @@ bool NodeInputDragger::IsStarted() const return input_.IsValid(); } -void NodeInputDragger::Start(const NodeKeyframeTrackReference &input, const rational &time) +void NodeInputDragger::Start(const NodeKeyframeTrackReference &input, const rational &time, bool create_key_on_all_tracks) { Q_ASSERT(!IsStarted()); @@ -52,9 +52,8 @@ void NodeInputDragger::Start(const NodeKeyframeTrackReference &input, const rati // Determine whether we are creating a keyframe or not if (input_.input().IsKeyframing()) { dragging_key_ = node->GetKeyframeAtTimeOnTrack(input_, time); - drag_created_key_ = !dragging_key_; - if (drag_created_key_) { + if (!dragging_key_) { dragging_key_ = new NodeKeyframe(time, start_value_, node->GetBestKeyframeTypeForTimeOnTrack(input_, time), @@ -62,6 +61,19 @@ void NodeInputDragger::Start(const NodeKeyframeTrackReference &input, const rati input_.input().element(), input_.input().input(), node); + created_keys_.append(dragging_key_); + + if (create_key_on_all_tracks) { + int nb_tracks = NodeValue::get_number_of_keyframe_tracks(input.input().node()->GetInputDataType(input.input().input())); + for (int i=0; iGetSplitValueAtTimeOnTrack(this_ref, time), + node->GetBestKeyframeTypeForTimeOnTrack(this_ref, time), + i, input.input().element(), input.input().input(), node)); + } + } + } } } } @@ -112,9 +124,9 @@ void NodeInputDragger::End() MultiUndoCommand* command = new MultiUndoCommand(); if (input_.input().node()->IsInputKeyframing(input_.input())) { - if (drag_created_key_) { + for (int i=0; iadd_child(new NodeParamInsertKeyframeCommand(input_.input().node(), dragging_key_)); + command->add_child(new NodeParamInsertKeyframeCommand(input_.input().node(), created_keys_.at(i))); } // We just set a keyframe's value @@ -129,6 +141,7 @@ void NodeInputDragger::End() Core::instance()->undo_stack()->push(command); input_.Reset(); + created_keys_.clear(); } } diff --git a/app/node/inputdragger.h b/app/node/inputdragger.h index 8743e7aff..bf74068da 100644 --- a/app/node/inputdragger.h +++ b/app/node/inputdragger.h @@ -34,7 +34,7 @@ public: bool IsStarted() const; - void Start(const NodeKeyframeTrackReference& input, const rational& time); + void Start(const NodeKeyframeTrackReference& input, const rational& time, bool create_key_on_all_tracks = true); void Drag(QVariant value); @@ -50,8 +50,7 @@ private: QVariant end_value_; NodeKeyframe* dragging_key_; - - bool drag_created_key_; + QVector created_keys_; }; diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index a6b1240b7..17411ace1 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -207,14 +207,25 @@ void NodeParamViewWidgetBridge::SetInputValueInternal(const QVariant &value, int command->add_child(new NodeParamSetKeyframeValueCommand(existing_key, value)); } else { // No existing key, create a new one - NodeKeyframe* new_key = new NodeKeyframe(node_time, - value, - input_.node()->GetBestKeyframeTypeForTimeOnTrack(NodeKeyframeTrackReference(input_, track), node_time), - track, - input_.element(), - input_.input()); + int nb_tracks = NodeValue::get_number_of_keyframe_tracks(input_.node()->GetInputDataType(input_.input())); + for (int i=0; iadd_child(new NodeParamInsertKeyframeCommand(input_.node(), new_key)); + if (i == track) { + track_value = value; + } else { + track_value = input_.node()->GetValueAtTime(input_.input(), node_time, input_.element()); + } + + NodeKeyframe* new_key = new NodeKeyframe(node_time, + track_value, + input_.node()->GetBestKeyframeTypeForTimeOnTrack(NodeKeyframeTrackReference(input_, i), node_time), + i, + input_.element(), + input_.input()); + + command->add_child(new NodeParamInsertKeyframeCommand(input_.node(), new_key)); + } } } else { command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(input_, track), value)); From 8c18b58bbb916909dd29d215946e932dc3770705 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 19:58:37 +1000 Subject: [PATCH 36/45] nodeparamview: pick up move event Fixes #1547 --- app/widget/nodeparamview/nodeparamview.cpp | 7 ++++--- app/widget/nodeparamview/nodeparamviewitem.cpp | 15 ++++++++++++--- app/widget/nodeparamview/nodeparamviewitem.h | 4 ++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 2df568671..f05c4996a 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -257,7 +257,7 @@ void NodeParamView::UpdateItemTime(const int64_t ×tamp) void NodeParamView::QueueKeyframePositionUpdate() { - QMetaObject::invokeMethod(this, "UpdateElementY", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &NodeParamView::UpdateElementY, Qt::QueuedConnection); } void NodeParamView::SignalNodeOrder() @@ -305,8 +305,9 @@ void NodeParamView::AddNode(Node *n) connect(item, &NodeParamViewItem::dockLocationChanged, this, &NodeParamView::QueueKeyframePositionUpdate); connect(item, &NodeParamViewItem::dockLocationChanged, this, &NodeParamView::SignalNodeOrder); connect(item, &NodeParamViewItem::PinToggled, this, &NodeParamView::PinNode); - connect(item, &NodeParamViewItem::ExpandedChanged, this, &NodeParamView::UpdateElementY); - connect(item, &NodeParamViewItem::ArrayExpandedChanged, this, &NodeParamView::UpdateElementY); + connect(item, &NodeParamViewItem::ArrayExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); + connect(item, &NodeParamViewItem::ExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); + connect(item, &NodeParamViewItem::Moved, this, &NodeParamView::QueueKeyframePositionUpdate); // Set time target item->SetTimeTarget(GetTimeTarget()); diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 70b96e1c4..a9463b6e8 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -36,8 +36,10 @@ const int NodeParamViewItemBody::kKeyControlColumn = 10; const int NodeParamViewItemBody::kArrayInsertColumn = kKeyControlColumn-1; const int NodeParamViewItemBody::kArrayRemoveColumn = kArrayInsertColumn-1; +#define super QDockWidget + NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : - QDockWidget(parent), + super(parent), node_(node), highlighted_(false) { @@ -92,12 +94,12 @@ void NodeParamViewItem::changeEvent(QEvent *e) Retranslate(); } - QWidget::changeEvent(e); + super::changeEvent(e); } void NodeParamViewItem::paintEvent(QPaintEvent *event) { - QDockWidget::paintEvent(event); + super::paintEvent(event); // Draw border if focused if (highlighted_) { @@ -108,6 +110,13 @@ void NodeParamViewItem::paintEvent(QPaintEvent *event) } } +void NodeParamViewItem::moveEvent(QMoveEvent *event) +{ + super::moveEvent(event); + + emit Moved(); +} + void NodeParamViewItem::Retranslate() { node_->Retranslate(); diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index 2a72134ef..b8c9b0a19 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -188,11 +188,15 @@ signals: void ArrayExpandedChanged(bool e); + void Moved(); + protected: virtual void changeEvent(QEvent *e) override; virtual void paintEvent(QPaintEvent *event) override; + virtual void moveEvent(QMoveEvent *event) override; + private: NodeParamViewItemTitleBar* title_bar_; From 82d2a253d94173b781d75fb1574cfb1b418e37a2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 11 Apr 2021 20:20:01 +1000 Subject: [PATCH 37/45] preferences: revised audio tab Cleaned up and improved the audio tab. Fixes #1546. Kind of. There doesn't seem to be much need for a sample rate selection for our current high-level audio system. If we implement higher end audio backends in the future (e.g. ASIO, JACK), those can be given a sample rate setting. --- app/audio/audiomanager.cpp | 12 ++ app/audio/audiomanager.h | 7 ++ .../tabs/preferencesappearancetab.cpp | 1 - .../preferences/tabs/preferencesaudiotab.cpp | 114 +++++++++++------- .../preferences/tabs/preferencesaudiotab.h | 7 +- .../tabs/preferencesbehaviortab.cpp | 1 - .../tabs/preferencesgeneraltab.cpp | 1 - .../tabs/preferenceskeyboardtab.cpp | 1 - 8 files changed, 90 insertions(+), 54 deletions(-) diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index d4716a7f9..a3ff8c4e8 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -28,6 +28,18 @@ namespace olive { AudioManager* AudioManager::instance_ = nullptr; +QString AudioManager::GetAudioBackendName(AudioManager::Backend b) +{ + switch (b) { + case kAudioBackendQt: + return tr("Qt"); + case kAudioBackendCount: + break; + } + + return tr("Unknown"); +} + void AudioManager::CreateInstance() { if (instance_ == nullptr) { diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index 26503e26c..3e159d255 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -44,6 +44,13 @@ class AudioManager : public QObject { Q_OBJECT public: + enum Backend { + kAudioBackendQt, + kAudioBackendCount + }; + + static QString GetAudioBackendName(Backend b); + static void CreateInstance(); static void DestroyInstance(); diff --git a/app/dialog/preferences/tabs/preferencesappearancetab.cpp b/app/dialog/preferences/tabs/preferencesappearancetab.cpp index 6bc54e753..e7fc9058f 100644 --- a/app/dialog/preferences/tabs/preferencesappearancetab.cpp +++ b/app/dialog/preferences/tabs/preferencesappearancetab.cpp @@ -36,7 +36,6 @@ namespace olive { PreferencesAppearanceTab::PreferencesAppearanceTab() { QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); QGridLayout* appearance_layout = new QGridLayout(); layout->addLayout(appearance_layout); diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.cpp b/app/dialog/preferences/tabs/preferencesaudiotab.cpp index 7ee42c0fa..079fa1e74 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.cpp +++ b/app/dialog/preferences/tabs/preferencesaudiotab.cpp @@ -21,6 +21,7 @@ #include "preferencesaudiotab.h" #include +#include #include #include "audio/audiomanager.h" @@ -30,65 +31,88 @@ namespace olive { PreferencesAudioTab::PreferencesAudioTab() { - QGridLayout* audio_tab_layout = new QGridLayout(this); - audio_tab_layout->setMargin(0); + QVBoxLayout* audio_tab_layout = new QVBoxLayout(this); - int row = 0; + { + // Backend Layout + QGridLayout* main_layout = new QGridLayout(); + main_layout->setMargin(0); - // Audio -> Output Device - audio_tab_layout->addWidget(new QLabel(tr("Output Device:")), row, 0); + int row = 0; - audio_output_devices_ = new QComboBox(); - audio_tab_layout->addWidget(audio_output_devices_, row, 1); + main_layout->addWidget(new QLabel(tr("Backend:")), row, 0); - row++; - - // Audio -> Input Device - audio_tab_layout->addWidget(new QLabel(tr("Input Device:")), row, 0); - - audio_input_devices_ = new QComboBox(); - audio_tab_layout->addWidget(audio_input_devices_, row, 1); - - row++; - - // Audio -> Sample Rate - - audio_tab_layout->addWidget(new QLabel(tr("Sample Rate:")), row, 0); - - audio_sample_rate_ = new QComboBox(); - /*combobox_audio_sample_rates(audio_sample_rate); - for (int i=0;icount();i++) { - if (audio_sample_rate->itemData(i).toInt() == olive::config.audio_rate) { - audio_sample_rate->setCurrentIndex(i); - break; + audio_backend_combobox_ = new QComboBox(); + for (int i=0; iaddItem(AudioManager::GetAudioBackendName(static_cast(i))); } - }*/ + main_layout->addWidget(audio_backend_combobox_, row, 1); - audio_tab_layout->addWidget(audio_sample_rate_, row, 1); + audio_tab_layout->addLayout(main_layout); + } - row++; + { + // Qt-Backend Layout + QGroupBox* qt_groupbox = new QGroupBox(); + audio_tab_layout->addWidget(qt_groupbox); - // Audio -> Audio Recording - audio_tab_layout->addWidget(new QLabel(tr("Audio Recording:"), this), row, 0); + QVBoxLayout* qt_layout = new QVBoxLayout(qt_groupbox); - recording_combobox_ = new QComboBox(); - recording_combobox_->addItem(tr("Mono")); - recording_combobox_->addItem(tr("Stereo")); -// recordingComboBox->setCurrentIndex(olive::config.recording_mode - 1); - audio_tab_layout->addWidget(recording_combobox_, row, 1); + int row = 0; - row++; + { + // Output Group + QGroupBox* qt_output_group = new QGroupBox(); + qt_output_group->setTitle(tr("Output")); + qt_layout->addWidget(qt_output_group); - refresh_devices_btn_ = new QPushButton(tr("Refresh Devices")); - audio_tab_layout->addWidget(refresh_devices_btn_, row, 1); + QGridLayout* qt_output_layout = new QGridLayout(qt_output_group); - row++; + qt_output_layout->addWidget(new QLabel(tr("Device:")), row, 0); - RetrieveDeviceLists(); + audio_output_devices_ = new QComboBox(); + qt_output_layout->addWidget(audio_output_devices_, row, 1); + } - connect(refresh_devices_btn_, &QPushButton::clicked, this, &PreferencesAudioTab::RefreshDevices); - connect(AudioManager::instance(), &AudioManager::OutputListReady, this, &PreferencesAudioTab::RetrieveOutputList); - connect(AudioManager::instance(), &AudioManager::InputListReady, this, &PreferencesAudioTab::RetrieveInputList); + row = 0; + + { + QGroupBox* qt_input_group = new QGroupBox(); + qt_input_group->setTitle(tr("Input")); + qt_layout->addWidget(qt_input_group); + + QGridLayout* qt_input_layout = new QGridLayout(qt_input_group); + + qt_input_layout->addWidget(new QLabel(tr("Device:")), row, 0); + + audio_input_devices_ = new QComboBox(); + qt_input_layout->addWidget(audio_input_devices_, row, 1); + + row++; + + qt_input_layout->addWidget(new QLabel(tr("Recording Mode:"), this), row, 0); + + recording_combobox_ = new QComboBox(); + recording_combobox_->addItem(tr("Mono")); + recording_combobox_->addItem(tr("Stereo")); + qt_input_layout->addWidget(recording_combobox_, row, 1); + } + + QHBoxLayout* qt_refresh_layout = new QHBoxLayout(); + qt_layout->addLayout(qt_refresh_layout); + qt_refresh_layout->addStretch(); + + refresh_devices_btn_ = new QPushButton(tr("Refresh Devices")); + qt_refresh_layout->addWidget(refresh_devices_btn_); + + RetrieveDeviceLists(); + + connect(refresh_devices_btn_, &QPushButton::clicked, this, &PreferencesAudioTab::RefreshDevices); + connect(AudioManager::instance(), &AudioManager::OutputListReady, this, &PreferencesAudioTab::RetrieveOutputList); + connect(AudioManager::instance(), &AudioManager::InputListReady, this, &PreferencesAudioTab::RetrieveInputList); + } + + audio_tab_layout->addStretch(); } void PreferencesAudioTab::Accept(MultiUndoCommand *command) diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.h b/app/dialog/preferences/tabs/preferencesaudiotab.h index ba0eed3eb..6a2feb2ec 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.h +++ b/app/dialog/preferences/tabs/preferencesaudiotab.h @@ -38,6 +38,8 @@ public: virtual void Accept(MultiUndoCommand* command) override; private: + QComboBox* audio_backend_combobox_; + /** * @brief UI widget for selecting the output audio device */ @@ -48,11 +50,6 @@ private: */ QComboBox* audio_input_devices_; - /** - * @brief UI widget for selecting the audio sampling rates - */ - QComboBox* audio_sample_rate_; - /** * @brief UI widget for editing the recording channels */ diff --git a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp index eee5680c1..e6ae2fcd8 100644 --- a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp +++ b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp @@ -30,7 +30,6 @@ namespace olive { PreferencesBehaviorTab::PreferencesBehaviorTab() { QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); behavior_tree_ = new QTreeWidget(); layout->addWidget(behavior_tree_); diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp index 1213fa67f..dc874debe 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp @@ -35,7 +35,6 @@ namespace olive { PreferencesGeneralTab::PreferencesGeneralTab() { QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); { QGroupBox* global_groupbox = new QGroupBox(tr("Locale")); diff --git a/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp b/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp index 1a4354999..3099a70f2 100644 --- a/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp +++ b/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp @@ -32,7 +32,6 @@ namespace olive { PreferencesKeyboardTab::PreferencesKeyboardTab(QMenuBar *menubar) { QVBoxLayout* shortcut_layout = new QVBoxLayout(this); - shortcut_layout->setMargin(0); QLineEdit* key_search_line = new QLineEdit(); key_search_line->setPlaceholderText(tr("Search for action or shortcut")); From 46ee99264d1af58d3f278b6c314fa5ece88e4f9b Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Apr 2021 09:48:08 +1000 Subject: [PATCH 38/45] footage: fixed crash with audio-only files --- app/node/output/viewer/viewer.cpp | 52 +++++++++++++++++++--------- app/node/output/viewer/viewer.h | 4 +++ app/node/project/footage/footage.cpp | 1 + 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 6b30cd3cc..81c89b383 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -38,7 +38,8 @@ const uint64_t ViewerOutput::kVideoParamEditMask = VideoParamEdit::kWidthHeight ViewerOutput::ViewerOutput(bool create_default_streams) : video_frame_cache_(this), - audio_playback_cache_(this) + audio_playback_cache_(this), + cache_enabled_(true) { AddInput(kVideoParamsInput, NodeValue::kVideoParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray)); SetInputProperty(kVideoParamsInput, QStringLiteral("mask"), QVariant::fromValue(kVideoParamEditMask)); @@ -195,14 +196,18 @@ void ViewerOutput::set_default_parameters() void ViewerOutput::ShiftVideoCache(const rational &from, const rational &to) { - video_frame_cache_.Shift(from, to); + if (cache_enabled_) { + video_frame_cache_.Shift(from, to); + } ShiftVideoEvent(from, to); } void ViewerOutput::ShiftAudioCache(const rational &from, const rational &to) { - audio_playback_cache_.Shift(from, to); + if (cache_enabled_) { + audio_playback_cache_.Shift(from, to); + } ShiftAudioEvent(from, to); } @@ -217,16 +222,18 @@ void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, { Q_UNUSED(element) - if (from == kTextureInput || from == kSamplesInput - || from == kVideoParamsInput || from == kAudioParamsInput) { - TimeRange invalidated_range(qMax(rational(), range.in()), - qMin(GetLength(), range.out())); + if (cache_enabled_) { + if (from == kTextureInput || from == kSamplesInput + || from == kVideoParamsInput || from == kAudioParamsInput) { + TimeRange invalidated_range(qMax(rational(), range.in()), + qMin(GetLength(), range.out())); - if (invalidated_range.in() != invalidated_range.out()) { - if (from == kTextureInput || from == kVideoParamsInput) { - video_frame_cache_.Invalidate(invalidated_range, job_time); - } else { - audio_playback_cache_.Invalidate(invalidated_range, job_time); + if (invalidated_range.in() != invalidated_range.out()) { + if (from == kTextureInput || from == kVideoParamsInput) { + video_frame_cache_.Invalidate(invalidated_range, job_time); + } else { + audio_playback_cache_.Invalidate(invalidated_range, job_time); + } } } } @@ -307,7 +314,9 @@ void ViewerOutput::VerifyLength() video_length = t.Get(NodeValue::kRational, QStringLiteral("length")).value(); } - video_frame_cache_.SetLength(video_length); + if (cache_enabled_) { + video_frame_cache_.SetLength(video_length); + } } { @@ -318,7 +327,9 @@ void ViewerOutput::VerifyLength() audio_length = t.Get(NodeValue::kRational, QStringLiteral("length")).value(); } - audio_playback_cache_.SetLength(audio_length); + if (cache_enabled_) { + audio_playback_cache_.SetLength(audio_length); + } } { @@ -392,7 +403,9 @@ void ViewerOutput::InputValueChangedEvent(const QString &input, int element) } if (frame_rate_changed) { - video_frame_cache_.SetTimebase(new_video_params.frame_rate_as_time_base()); + if (cache_enabled_) { + video_frame_cache_.SetTimebase(new_video_params.frame_rate_as_time_base()); + } emit FrameRateChanged(new_video_params.frame_rate()); } @@ -412,7 +425,9 @@ void ViewerOutput::InputValueChangedEvent(const QString &input, int element) emit AudioParamsChanged(); - audio_playback_cache_.SetParameters(GetAudioParams()); + if (cache_enabled_) { + audio_playback_cache_.SetParameters(GetAudioParams()); + } cached_audio_params_ = new_audio_params; @@ -514,6 +529,11 @@ int ViewerOutput::AddStream(Track::Type type, const QVariant& value) return index; } +void ViewerOutput::SetViewerCacheEnabled(bool e) +{ + cache_enabled_ = e; +} + void ViewerOutput::InputResized(const QString &input, int old_size, int new_size) { if (input == kVideoParamsInput || input == kAudioParamsInput) { diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 1872df8e8..a56602b49 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -188,6 +188,8 @@ protected: int AddStream(Track::Type type, const QVariant &value); + void SetViewerCacheEnabled(bool e); + private: rational last_length_; @@ -203,6 +205,8 @@ private: TimelinePoints timeline_points_; + bool cache_enabled_; + private slots: void InputResized(const QString& input, int old_size, int new_size); diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 796e96cfe..8344cca0d 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -50,6 +50,7 @@ Footage::Footage(const QString &filename) : set_filename(filename); SetCacheTextures(true); + SetViewerCacheEnabled(false); } void Footage::Retranslate() From 0afaf0a03df716360103b3b5598592f0d88d67b8 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Apr 2021 10:06:37 +1000 Subject: [PATCH 39/45] handmovableview: set interactive to false when dragging Fixes #1536 --- app/widget/handmovableview/handmovableview.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/widget/handmovableview/handmovableview.cpp b/app/widget/handmovableview/handmovableview.cpp index 6c3362e3c..a06a7b454 100644 --- a/app/widget/handmovableview/handmovableview.cpp +++ b/app/widget/handmovableview/handmovableview.cpp @@ -37,8 +37,10 @@ void HandMovableView::ApplicationToolChanged(Tool::Item tool) { if (tool == Tool::kHand) { setDragMode(ScrollHandDrag); + setInteractive(false); } else { setDragMode(default_drag_mode_); + setInteractive(true); } ToolChangedEvent(tool); @@ -51,6 +53,7 @@ bool HandMovableView::HandPress(QMouseEvent *event) dragging_hand_ = true; setDragMode(ScrollHandDrag); + setInteractive(false); // Transform mouse event to act like the left button is pressed QMouseEvent transformed(event->type(), @@ -94,6 +97,7 @@ bool HandMovableView::HandRelease(QMouseEvent *event) QGraphicsView::mouseReleaseEvent(&transformed); + setInteractive(true); setDragMode(pre_hand_drag_mode_); dragging_hand_ = false; From da9cdbd29e7162c289ec87abff34f3a2531d7f4d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Apr 2021 10:35:02 +1000 Subject: [PATCH 40/45] block: disable connecting and keyframing speed parameter We're going to replace keyframing this with a time remap node because that will be much simpler to implement. Fixes #1192. Fixes #1204. --- app/node/block/block.cpp | 39 ++++++++++++++-------------------- app/render/renderprocessor.cpp | 18 ++++++---------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 4ce6ec91b..8cee4b959 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -50,8 +50,9 @@ Block::Block() : AddInput(kEnabledInput, NodeValue::kBoolean, true, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); - AddInput(kSpeedInput, NodeValue::kFloat, 1.0); + AddInput(kSpeedInput, NodeValue::kFloat, 1.0, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); SetInputProperty(kSpeedInput, QStringLiteral("view"), FloatSlider::kPercentage); + SetInputProperty(kSpeedInput, QStringLiteral("min"), 0.0); IgnoreHashingFrom(kSpeedInput); // A block's length must be greater than 0 @@ -126,18 +127,14 @@ rational Block::SequenceToMediaTime(const rational &sequence_time) const rational local_time = sequence_time; // FIXME: Doesn't handle reversing - if (IsInputStatic(kSpeedInput)) { - double speed_value = GetStandardValue(kSpeedInput).toDouble(); + double speed_value = GetStandardValue(kSpeedInput).toDouble(); - if (qIsNull(speed_value)) { - // Effectively holds the frame at the in point - local_time = 0; - } else if (!qFuzzyCompare(speed_value, 1.0)) { - // Multiply time - local_time = rational::fromDouble(local_time.toDouble() * speed_value); - } - } else { - // FIXME: We'll need to calculate the speed hoo boy + if (qIsNull(speed_value)) { + // Effectively holds the frame at the in point + local_time = 0; + } else if (!qFuzzyCompare(speed_value, 1.0)) { + // Multiply time + local_time = rational::fromDouble(local_time.toDouble() * speed_value); } return local_time + media_in(); @@ -153,18 +150,14 @@ rational Block::MediaToSequenceTime(const rational &media_time) const rational sequence_time = media_time - media_in(); // FIXME: Doesn't handle reversing - if (IsInputKeyframing(kSpeedInput) || IsInputConnected(kSpeedInput)) { - // FIXME: We'll need to calculate the speed hoo boy - } else { - double speed_value = GetStandardValue(kSpeedInput).toDouble(); + double speed_value = GetStandardValue(kSpeedInput).toDouble(); - if (qIsNull(speed_value)) { - // Effectively holds the frame at the in point, also prevents divide by zero - sequence_time = 0; - } else if (!qFuzzyCompare(speed_value, 1.0)) { - // Multiply time - sequence_time = rational::fromDouble(sequence_time.toDouble() / speed_value); - } + if (qIsNull(speed_value)) { + // Effectively holds the frame at the in point, also prevents divide by zero + sequence_time = 0; + } else if (!qFuzzyCompare(speed_value, 1.0)) { + // Multiply time + sequence_time = rational::fromDouble(sequence_time.toDouble() / speed_value); } return sequence_time; diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 63bad549c..c600ad43d 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -227,18 +227,14 @@ NodeValueTable RenderProcessor::GenerateBlockTable(const Track *track, const Tim } // FIXME: Doesn't handle reversing - if (b->IsInputKeyframing(Block::kSpeedInput) || b->IsInputConnected(Block::kSpeedInput)) { - // FIXME: We'll need to calculate the speed hoo boy - } else { - double speed_value = b->GetStandardValue(Block::kSpeedInput).toDouble(); + double speed_value = b->GetStandardValue(Block::kSpeedInput).toDouble(); - if (qIsNull(speed_value)) { - // Just silence, don't think there's any other practical application of 0 speed audio - samples_from_this_block->fill(0); - } else if (!qFuzzyCompare(speed_value, 1.0)) { - // Multiply time - samples_from_this_block->speed(speed_value); - } + if (qIsNull(speed_value)) { + // Just silence, don't think there's any other practical application of 0 speed audio + samples_from_this_block->fill(0); + } else if (!qFuzzyCompare(speed_value, 1.0)) { + // Multiply time + samples_from_this_block->speed(speed_value); } int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count()); From 7701e9331fa0e1519d2cf6b14d074df9529c14f9 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Apr 2021 11:05:45 +1000 Subject: [PATCH 41/45] block: reimplemented reversing --- app/node/block/block.cpp | 40 ++++++++++++++++++++++++++-------- app/node/block/block.h | 13 ++++++++++- app/render/renderprocessor.cpp | 5 ++++- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 8cee4b959..ba8fedb90 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -32,6 +32,7 @@ const QString Block::kLengthInput = QStringLiteral("length_in"); const QString Block::kMediaInInput = QStringLiteral("media_in_in"); const QString Block::kEnabledInput = QStringLiteral("enabled_in"); const QString Block::kSpeedInput = QStringLiteral("speed_in"); +const QString Block::kReverseInput = QStringLiteral("reverse_in"); Block::Block() : previous_(nullptr), @@ -55,6 +56,9 @@ Block::Block() : SetInputProperty(kSpeedInput, QStringLiteral("min"), 0.0); IgnoreHashingFrom(kSpeedInput); + AddInput(kReverseInput, NodeValue::kBoolean, false, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); + IgnoreHashingFrom(kReverseInput); + // A block's length must be greater than 0 set_length_and_media_out(1); } @@ -77,6 +81,11 @@ void Block::set_length_and_media_out(const rational &length) return; } + if (reverse()) { + // Calculate media_in adjustment + set_media_in(SequenceToMediaTime(length - this->length(), true)); + } + set_length_internal(length); } @@ -88,8 +97,10 @@ void Block::set_length_and_media_in(const rational &length) return; } - // Calculate media_in adjustment - set_media_in(SequenceToMediaTime(this->length() - length)); + if (!reverse()) { + // Calculate media_in adjustment + set_media_in(SequenceToMediaTime(this->length() - length)); + } // Set the length without setting media out set_length_internal(length); @@ -117,7 +128,7 @@ void Block::set_enabled(bool e) emit EnabledChanged(); } -rational Block::SequenceToMediaTime(const rational &sequence_time) const +rational Block::SequenceToMediaTime(const rational &sequence_time, bool ignore_reverse) const { // These constants are not considered "values" per se, so we don't modify them if (sequence_time == RATIONAL_MIN || sequence_time == RATIONAL_MAX) { @@ -126,8 +137,7 @@ rational Block::SequenceToMediaTime(const rational &sequence_time) const rational local_time = sequence_time; - // FIXME: Doesn't handle reversing - double speed_value = GetStandardValue(kSpeedInput).toDouble(); + double speed_value = speed(); if (qIsNull(speed_value)) { // Effectively holds the frame at the in point @@ -137,7 +147,13 @@ rational Block::SequenceToMediaTime(const rational &sequence_time) const local_time = rational::fromDouble(local_time.toDouble() * speed_value); } - return local_time + media_in(); + rational media_time = local_time + media_in(); + + if (reverse() && !ignore_reverse) { + media_time = length() - media_time; + } + + return media_time; } rational Block::MediaToSequenceTime(const rational &media_time) const @@ -147,10 +163,15 @@ rational Block::MediaToSequenceTime(const rational &media_time) const return media_time; } - rational sequence_time = media_time - media_in(); + rational sequence_time = media_time; - // FIXME: Doesn't handle reversing - double speed_value = GetStandardValue(kSpeedInput).toDouble(); + if (reverse()) { + sequence_time = length() - sequence_time; + } + + sequence_time -= media_in(); + + double speed_value = speed(); if (qIsNull(speed_value)) { // Effectively holds the frame at the in point, also prevents divide by zero @@ -200,6 +221,7 @@ void Block::Retranslate() SetInputName(kMediaInInput, tr("Media In")); SetInputName(kEnabledInput, tr("Enabled")); SetInputName(kSpeedInput, tr("Speed")); + SetInputName(kReverseInput, tr("Reverse")); } void Block::Hash(const QString &, QCryptographicHash &, const rational &) const diff --git a/app/node/block/block.h b/app/node/block/block.h index 30b9e73ca..c246dfa05 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -151,12 +151,23 @@ public: return block_links_; } + double speed() const + { + return GetStandardValue(kSpeedInput).toDouble(); + } + + bool reverse() const + { + return GetStandardValue(kReverseInput).toBool(); + } + virtual void Hash(const QString& output, QCryptographicHash &hash, const rational &time) const override; static const QString kLengthInput; static const QString kMediaInInput; static const QString kEnabledInput; static const QString kSpeedInput; + static const QString kReverseInput; public slots: @@ -166,7 +177,7 @@ signals: void LengthChanged(); protected: - rational SequenceToMediaTime(const rational& sequence_time) const; + rational SequenceToMediaTime(const rational& sequence_time, bool ignore_reverse = false) const; rational MediaToSequenceTime(const rational& media_time) const; diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index c600ad43d..f41f5f25e 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -226,7 +226,6 @@ NodeValueTable RenderProcessor::GenerateBlockTable(const Track *track, const Tim continue; } - // FIXME: Doesn't handle reversing double speed_value = b->GetStandardValue(Block::kSpeedInput).toDouble(); if (qIsNull(speed_value)) { @@ -237,6 +236,10 @@ NodeValueTable RenderProcessor::GenerateBlockTable(const Track *track, const Tim samples_from_this_block->speed(speed_value); } + if (b->GetStandardValue(Block::kReverseInput).toBool()) { + samples_from_this_block->reverse(); + } + int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count()); // Copy samples into destination buffer From c48d2038d4aa1666d76a04dc84f68bb5ab3eb1cc Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Apr 2021 12:02:48 +1000 Subject: [PATCH 42/45] reimplemented saving keyboard shortcuts Fixes #1412 --- app/window/mainwindow/mainwindow.cpp | 105 +++++++++++++++++++++++++++ app/window/mainwindow/mainwindow.h | 6 ++ 2 files changed, 111 insertions(+) diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 0cb3a4128..8cc4c3e1e 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -68,6 +68,8 @@ MainWindow::MainWindow(QWidget *parent) : MainMenu* main_menu = new MainMenu(this); setMenuBar(main_menu); + LoadCustomShortcuts(); + // Create and set status bar MainStatusBar* status_bar = new MainStatusBar(this); status_bar->ConnectTaskManager(TaskManager::instance()); @@ -406,6 +408,8 @@ void MainWindow::closeEvent(QCloseEvent *e) PanelManager::instance()->DeleteAllPanels(); + SaveCustomShortcuts(); + QMainWindow::closeEvent(e); } @@ -547,6 +551,107 @@ void MainWindow::TimelineFocused(ViewerOutput* viewer) curve_panel_->ConnectViewerNode(viewer); } +QString MainWindow::GetCustomShortcutsFile() +{ + return QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("shortcuts")); +} + +void LoadCustomShortcutsInternal(QMenu* menu, const QMap& shortcuts) +{ + QList actions = menu->actions(); + + foreach (QAction* a, actions) { + if (a->menu()) { + LoadCustomShortcutsInternal(a->menu(), shortcuts); + } else if (!a->isSeparator()) { + QString action_id = a->property("id").toString(); + + if (shortcuts.contains(action_id)) { + a->setShortcut(shortcuts.value(action_id)); + } + } + } +} + +void MainWindow::LoadCustomShortcuts() +{ + QFile shortcut_file(GetCustomShortcutsFile()); + if (shortcut_file.exists() && shortcut_file.open(QFile::ReadOnly)) { + QMap shortcuts; + + QString shortcut_str = QString::fromUtf8(shortcut_file.readAll()); + + QStringList shortcut_list = shortcut_str.split(QStringLiteral("\n")); + + foreach (const QString& s, shortcut_list) { + QStringList shortcut_line = s.split(QStringLiteral("\t")); + if (shortcut_line.size() >= 2) { + shortcuts.insert(shortcut_line.at(0), shortcut_line.at(1)); + } + } + + shortcut_file.close(); + + if (!shortcuts.isEmpty()) { + QList menus = menuBar()->actions(); + + foreach (QAction* menu, menus) { + LoadCustomShortcutsInternal(menu->menu(), shortcuts); + } + } + } +} + +void SaveCustomShortcutsInternal(QMenu* menu, QMap* shortcuts) +{ + QList actions = menu->actions(); + + foreach (QAction* a, actions) { + if (a->menu()) { + SaveCustomShortcutsInternal(a->menu(), shortcuts); + } else if (!a->isSeparator()) { + QString default_shortcut = a->property("keydefault").toString(); + QString current_shortcut = a->shortcut().toString(); + if (current_shortcut != default_shortcut) { + QString action_id = a->property("id").toString(); + shortcuts->insert(action_id, current_shortcut); + } + } + } +} + +void MainWindow::SaveCustomShortcuts() +{ + QMap shortcuts; + QList menus = menuBar()->actions(); + + foreach (QAction* menu, menus) { + SaveCustomShortcutsInternal(menu->menu(), &shortcuts); + } + + QFile shortcut_file(GetCustomShortcutsFile()); + if (shortcuts.isEmpty()) { + if (shortcut_file.exists()) { + // No custom shortcuts, remove any existing file + shortcut_file.remove(); + } + } else if (shortcut_file.open(QFile::WriteOnly)) { + for (auto it=shortcuts.cbegin(); it!=shortcuts.cend(); it++) { + if (it != shortcuts.cbegin()) { + shortcut_file.write(QStringLiteral("\n").toUtf8()); + } + + shortcut_file.write(it.key().toUtf8()); + shortcut_file.write(QStringLiteral("\t").toUtf8()); + shortcut_file.write(it.value().toUtf8()); + } + shortcut_file.close(); + } else { + qCritical() << "Failed to save custom keyboard shortcuts"; + } + +} + void MainWindow::FocusedPanelChanged(PanelWidget *panel) { TimelinePanel* timeline = dynamic_cast(panel); diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index a31cc6b30..35db59aee 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -132,6 +132,12 @@ private: void TimelineFocused(ViewerOutput *viewer); + static QString GetCustomShortcutsFile(); + + void LoadCustomShortcuts(); + + void SaveCustomShortcuts(); + QByteArray premaximized_state_; // Standard panels From 6d0e1883936270de4a204a424f447cc88927dc3b Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Apr 2021 15:28:33 +1000 Subject: [PATCH 43/45] renderer: don't process video footage if rendering audio Fixes #1530 --- app/render/renderprocessor.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index f41f5f25e..79c6a5d43 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -273,6 +273,11 @@ NodeValueTable RenderProcessor::GenerateBlockTable(const Track *track, const Tim QVariant RenderProcessor::ProcessVideoFootage(const FootageJob &stream, const rational &input_time) { + if (ticket_->property("type").value() != RenderManager::kTypeVideo) { + // Video cannot contribute to audio, so we do nothing here + return QVariant(); + } + TexturePtr value = nullptr; // Check the still frame cache. On large frames such as high resolution still images, uploading From 893adc3d7241236fc4b7aadc130e4548c6b16d69 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Apr 2021 16:35:10 +1000 Subject: [PATCH 44/45] implemented value node Also implements changes necessary to support an input data type that changes. Much of that foundation was built in `nodearchchanges`, but hadn't been finalized. This commit finalizes and provides a reference implementation/test with the "Value" node. Fixes #1443 --- app/node/factory.cpp | 3 + app/node/factory.h | 1 + app/node/input/CMakeLists.txt | 1 + app/node/input/multicam/multicamnode.cpp | 6 ++ app/node/input/multicam/multicamnode.h | 11 +++ app/node/input/value/CMakeLists.txt | 22 +++++ app/node/input/value/valuenode.cpp | 81 +++++++++++++++++++ app/node/input/value/valuenode.h | 78 ++++++++++++++++++ app/node/inputimmediate.cpp | 18 +++-- app/node/inputimmediate.h | 7 ++ app/node/node.cpp | 18 ++++- .../nodeparamview/nodeparamviewitem.cpp | 32 +++++++- app/widget/nodeparamview/nodeparamviewitem.h | 8 ++ .../nodeparamviewwidgetbridge.cpp | 17 ++++ .../nodeparamview/nodeparamviewwidgetbridge.h | 4 + 15 files changed, 293 insertions(+), 14 deletions(-) create mode 100644 app/node/input/multicam/multicamnode.cpp create mode 100644 app/node/input/multicam/multicamnode.h create mode 100644 app/node/input/value/CMakeLists.txt create mode 100644 app/node/input/value/valuenode.cpp create mode 100644 app/node/input/value/valuenode.h diff --git a/app/node/factory.cpp b/app/node/factory.cpp index d0c468893..33c5a8996 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -46,6 +46,7 @@ #include "project/folder/folder.h" #include "project/footage/footage.h" #include "project/sequence/sequence.h" +#include "node/input/value/valuenode.h" namespace olive { QList NodeFactory::library_; @@ -234,6 +235,8 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) return new Folder(); case kProjectSequence: return new Sequence(); + case kValueNode: + return new ValueNode(); case kInternalNodeCount: break; diff --git a/app/node/factory.h b/app/node/factory.h index 78ef3bfbe..7d19977ce 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -56,6 +56,7 @@ public: kProjectFootage, kProjectFolder, kProjectSequence, + kValueNode, // Count value kInternalNodeCount diff --git a/app/node/input/CMakeLists.txt b/app/node/input/CMakeLists.txt index 5c95bb00d..c07a2bed6 100644 --- a/app/node/input/CMakeLists.txt +++ b/app/node/input/CMakeLists.txt @@ -15,6 +15,7 @@ # along with this program. If not, see . add_subdirectory(time) +add_subdirectory(value) set(OLIVE_SOURCES ${OLIVE_SOURCES} diff --git a/app/node/input/multicam/multicamnode.cpp b/app/node/input/multicam/multicamnode.cpp new file mode 100644 index 000000000..0c1273cb5 --- /dev/null +++ b/app/node/input/multicam/multicamnode.cpp @@ -0,0 +1,6 @@ +#include "multicamnode.h" + +MultiCamNode::MultiCamNode() +{ + +} diff --git a/app/node/input/multicam/multicamnode.h b/app/node/input/multicam/multicamnode.h new file mode 100644 index 000000000..c21e7f6ca --- /dev/null +++ b/app/node/input/multicam/multicamnode.h @@ -0,0 +1,11 @@ +#ifndef MULTICAMNODE_H +#define MULTICAMNODE_H + + +class MultiCamNode +{ +public: + MultiCamNode(); +}; + +#endif // MULTICAMNODE_H diff --git a/app/node/input/value/CMakeLists.txt b/app/node/input/value/CMakeLists.txt new file mode 100644 index 000000000..52ea76185 --- /dev/null +++ b/app/node/input/value/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2020 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} + node/input/value/valuenode.h + node/input/value/valuenode.cpp + PARENT_SCOPE +) diff --git a/app/node/input/value/valuenode.cpp b/app/node/input/value/valuenode.cpp new file mode 100644 index 000000000..5143c6ac8 --- /dev/null +++ b/app/node/input/value/valuenode.cpp @@ -0,0 +1,81 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 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 "valuenode.h" + +namespace olive { + +const QString ValueNode::kTypeInput = QStringLiteral("type_in"); +const QString ValueNode::kValueInput = QStringLiteral("value_in"); +const QVector ValueNode::kSupportedTypes = { + NodeValue::kFloat, + NodeValue::kInt, + NodeValue::kRational, + NodeValue::kVec2, + NodeValue::kVec3, + NodeValue::kVec4, + NodeValue::kColor, + NodeValue::kText, + NodeValue::kMatrix, + NodeValue::kFont, +}; + +#define super Node + +ValueNode::ValueNode() +{ + AddInput(kTypeInput, NodeValue::kCombo, 0, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); + + AddInput(kValueInput, kSupportedTypes.first(), QVariant(), InputFlags(kInputFlagNotConnectable)); +} + +void ValueNode::Retranslate() +{ + SetInputName(kTypeInput, QStringLiteral("Type")); + SetInputName(kValueInput, QStringLiteral("Value")); + + QStringList type_names; + type_names.reserve(kSupportedTypes.size()); + foreach (NodeValue::Type type, kSupportedTypes) { + type_names.append(NodeValue::GetPrettyDataTypeName(type)); + } + SetComboBoxStrings(kTypeInput, type_names); +} + +NodeValueTable ValueNode::Value(const QString &output, NodeValueDatabase &value) const +{ + Q_UNUSED(output) + + // Pop combobox value off table because no other node will need it + value[kTypeInput].Take(NodeValue::kCombo); + + return value.Merge(); +} + +void ValueNode::InputValueChangedEvent(const QString &input, int element) +{ + if (input == kTypeInput) { + SetInputDataType(kValueInput, kSupportedTypes.at(GetStandardValue(kTypeInput).toInt())); + } + + super::InputValueChangedEvent(input, element); +} + +} diff --git a/app/node/input/value/valuenode.h b/app/node/input/value/valuenode.h new file mode 100644 index 000000000..6d2a6b78e --- /dev/null +++ b/app/node/input/value/valuenode.h @@ -0,0 +1,78 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 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 VALUENODE_H +#define VALUENODE_H + +#include "node/node.h" + +namespace olive { + +class ValueNode : public Node +{ + Q_OBJECT +public: + ValueNode(); + + NODE_DEFAULT_DESTRUCTOR(ValueNode) + + virtual Node* copy() const override + { + return new ValueNode(); + } + + virtual QString Name() const override + { + return tr("Value"); + } + + virtual QString id() const override + { + return QStringLiteral("org.olivevideoeditor.Olive.value"); + } + + virtual QVector Category() const override + { + return {kCategoryInput}; + } + + virtual QString Description() const override + { + return tr("Create a single value that can be connected to various other inputs."); + } + + static const QString kTypeInput; + static const QString kValueInput; + + virtual void Retranslate() override; + + virtual NodeValueTable Value(const QString &output, NodeValueDatabase &value) const override; + +protected: + virtual void InputValueChangedEvent(const QString &input, int element) override; + +private: + static const QVector kSupportedTypes; + +}; + +} + +#endif // VALUENODE_H diff --git a/app/node/inputimmediate.cpp b/app/node/inputimmediate.cpp index d3fae09ae..e0d2a0187 100644 --- a/app/node/inputimmediate.cpp +++ b/app/node/inputimmediate.cpp @@ -27,14 +27,10 @@ namespace olive { NodeInputImmediate::NodeInputImmediate(NodeValue::Type type, const SplitValue &default_val) : + default_value_(default_val), keyframing_(false) { - int track_size = NodeValue::get_number_of_keyframe_tracks(type); - - keyframe_tracks_.resize(track_size); - standard_value_.resize(track_size); - - set_split_standard_value(default_val); + set_data_type(type); } void NodeInputImmediate::set_standard_value_on_track(const QVariant &value, int track) @@ -179,6 +175,16 @@ bool NodeInputImmediate::has_keyframe_at_time(const rational &time) const return false; } +void NodeInputImmediate::set_data_type(NodeValue::Type type) +{ + int track_size = NodeValue::get_number_of_keyframe_tracks(type); + + keyframe_tracks_.resize(track_size); + standard_value_.resize(track_size); + + set_split_standard_value(default_value_); +} + NodeKeyframe *NodeInputImmediate::get_earliest_keyframe() const { NodeKeyframe* earliest = nullptr; diff --git a/app/node/inputimmediate.h b/app/node/inputimmediate.h index ff6c2794c..e69467f31 100644 --- a/app/node/inputimmediate.h +++ b/app/node/inputimmediate.h @@ -151,12 +151,19 @@ public: return (!is_keyframing() || keyframe_tracks_.at(track).isEmpty()); } + void set_data_type(NodeValue::Type type); + private: /** * @brief Non-keyframed value */ SplitValue standard_value_; + /** + * @brief Default value + */ + SplitValue default_value_; + /** * @brief Internal keyframe array * diff --git a/app/node/node.cpp b/app/node/node.cpp index 1f4f4326b..c0678db5a 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -505,10 +505,15 @@ NodeValue::Type Node::GetInputDataType(const QString &id) const void Node::SetInputDataType(const QString &id, const NodeValue::Type &type) { - Input* i = GetInternalInputData(id); + Input* input_meta = GetInternalInputData(id); - if (i) { - i->type = type; + if (input_meta) { + input_meta->type = type; + + int array_sz = InputArraySize(id); + for (int i=-1; iset_data_type(type); + } emit InputDataTypeChanged(id, type); } else { @@ -693,7 +698,12 @@ SplitValue Node::GetSplitDefaultValue(const QString &input) const QVariant Node::GetSplitDefaultValueOnTrack(const QString &input, int track) const { - return GetSplitDefaultValue(input).at(track); + SplitValue val = GetSplitDefaultValue(input); + if (track < val.size()) { + return val.at(track); + } else { + return QVariant(); + } } const QVector &Node::GetKeyframeTracks(const QString &input, int element) const diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index a9463b6e8..fe92606f7 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -36,6 +36,9 @@ const int NodeParamViewItemBody::kKeyControlColumn = 10; const int NodeParamViewItemBody::kArrayInsertColumn = kKeyControlColumn-1; const int NodeParamViewItemBody::kArrayRemoveColumn = kArrayInsertColumn-1; +// 0 is for the array collapse button, 1 is for the main label, widgets start at 2 +const int NodeParamViewItemBody::kWidgetStartColumn = 2; + #define super QDockWidget NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : @@ -261,6 +264,10 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const InputUI ui_objects; + // Store layout and row + ui_objects.layout = layout; + ui_objects.row = row; + // Add descriptor label ui_objects.main_label = new QLabel(); @@ -303,23 +310,24 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const // Create a widget/input bridge for this input ui_objects.widget_bridge = new NodeParamViewWidgetBridge(NodeInput(node, input, element), this); + connect(ui_objects.widget_bridge, &NodeParamViewWidgetBridge::WidgetsRecreated, this, &NodeParamViewItemBody::ReplaceWidgets); connect(ui_objects.widget_bridge, &NodeParamViewWidgetBridge::ArrayWidgetDoubleClicked, this, &NodeParamViewItemBody::ToggleArrayExpanded); - // 0 is for the array collapse button, 1 is for the main label, widgets start at 2 - const int widget_start = 2; + // Place widgets into layout + PlaceWidgetsFromBridge(layout, ui_objects.widget_bridge, row); // Add widgets for this parameter to the layout for (int i=0; iwidgets().size(); i++) { QWidget* w = ui_objects.widget_bridge->widgets().at(i); - layout->addWidget(w, row, i+widget_start); + layout->addWidget(w, row, i+kWidgetStartColumn); } if (node->IsInputConnectable(input)) { // Create clickable label used when an input is connected ui_objects.connected_label = new NodeParamViewConnectedLabel(input_ref); connect(ui_objects.connected_label, &NodeParamViewConnectedLabel::RequestSelectNode, this, &NodeParamViewItemBody::RequestSelectNode); - layout->addWidget(ui_objects.connected_label, row, widget_start); + layout->addWidget(ui_objects.connected_label, row, kWidgetStartColumn); } // Add keyframe control to this layout if parameter is keyframable @@ -418,6 +426,16 @@ void NodeParamViewItemBody::UpdateUIForEdgeConnection(const NodeInput& input) } } +void NodeParamViewItemBody::PlaceWidgetsFromBridge(QGridLayout* layout, NodeParamViewWidgetBridge *bridge, int row) +{ + // Add widgets for this parameter to the layout + for (int i=0; iwidgets().size(); i++) { + QWidget* w = bridge->widgets().at(i); + + layout->addWidget(w, row, i+kWidgetStartColumn); + } +} + void NodeParamViewItemBody::ArrayCollapseBtnPressed(bool checked) { const NodeInputPair& input = array_collapse_buttons_.key(static_cast(sender())); @@ -514,6 +532,12 @@ void NodeParamViewItemBody::ToggleArrayExpanded() } } +void NodeParamViewItemBody::ReplaceWidgets(const NodeInput &input) +{ + InputUI ui = input_ui_map_.value(input); + PlaceWidgetsFromBridge(ui.layout, ui.widget_bridge, ui.row); +} + NodeParamViewItemBody::InputUI::InputUI() : main_label(nullptr), widget_bridge(nullptr), diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index b8c9b0a19..3351682dd 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -97,6 +97,8 @@ private: void UpdateUIForEdgeConnection(const NodeInput &input); + void PlaceWidgetsFromBridge(QGridLayout *layout, NodeParamViewWidgetBridge* bridge, int row); + struct InputUI { InputUI(); @@ -104,6 +106,8 @@ private: NodeParamViewWidgetBridge* widget_bridge; NodeParamViewConnectedLabel* connected_label; NodeParamViewKeyframeControl* key_control; + QGridLayout* layout; + int row; NodeParamViewArrayButton* array_insert_btn; NodeParamViewArrayButton* array_remove_btn; @@ -132,6 +136,8 @@ private: static const int kArrayInsertColumn; static const int kArrayRemoveColumn; + static const int kWidgetStartColumn; + private slots: void EdgeChanged(const NodeOutput &output, const NodeInput &input); @@ -147,6 +153,8 @@ private slots: void ToggleArrayExpanded(); + void ReplaceWidgets(const NodeInput& input); + }; class NodeParamViewItem : public QDockWidget diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 17411ace1..9c0254344 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -49,6 +49,7 @@ NodeParamViewWidgetBridge::NodeParamViewWidgetBridge(const NodeInput &input, QOb connect(input_.node(), &Node::ValueChanged, this, &NodeParamViewWidgetBridge::InputValueChanged); connect(input_.node(), &Node::InputPropertyChanged, this, &NodeParamViewWidgetBridge::PropertyChanged); + connect(input_.node(), &Node::InputDataTypeChanged, this, &NodeParamViewWidgetBridge::InputDataTypeChanged); } void NodeParamViewWidgetBridge::SetTime(const rational &time) @@ -756,6 +757,22 @@ void NodeParamViewWidgetBridge::PropertyChanged(const QString& input, const QStr } } +void NodeParamViewWidgetBridge::InputDataTypeChanged(const QString &input, NodeValue::Type type) +{ + Q_UNUSED(type) + if (input == this->input_.input()) { + // Delete all widgets + qDeleteAll(widgets_); + widgets_.clear(); + + // Create new widgets + CreateWidgets(); + + // Signal that widgets are new + emit WidgetsRecreated(input_); + } +} + bool NodeParamViewScrollBlocker::eventFilter(QObject *watched, QEvent *event) { Q_UNUSED(watched) diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h index 7b191f143..ee288f35f 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h @@ -52,6 +52,8 @@ public: signals: void ArrayWidgetDoubleClicked(); + void WidgetsRecreated(const NodeInput& input); + private: void CreateWidgets(); @@ -85,6 +87,8 @@ private slots: void PropertyChanged(const QString &input, const QString& key, const QVariant& value); + void InputDataTypeChanged(const QString& input, NodeValue::Type type); + }; } From 2c47462f8fad1c0ceb81945482be2a536871521f Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 12 Apr 2021 17:01:29 +1000 Subject: [PATCH 45/45] add hidden options to our CLI parser to ignore Qt's parameters Fixes #1511 --- app/common/commandlineparser.cpp | 8 ++++++-- app/common/commandlineparser.h | 13 ++++++++++++- app/main.cpp | 31 +++++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/app/common/commandlineparser.cpp b/app/common/commandlineparser.cpp index 7013276cd..d2e1b070b 100644 --- a/app/common/commandlineparser.cpp +++ b/app/common/commandlineparser.cpp @@ -34,11 +34,11 @@ CommandLineParser::~CommandLineParser() } } -const CommandLineParser::Option *CommandLineParser::AddOption(const QStringList &strings, const QString &description, bool takes_arg, const QString &arg_placeholder) +const CommandLineParser::Option *CommandLineParser::AddOption(const QStringList &strings, const QString &description, bool takes_arg, const QString &arg_placeholder, bool hidden) { Option* o = new Option(); - options_.append({strings, description, o, takes_arg, arg_placeholder}); + options_.append({strings, description, o, takes_arg, arg_placeholder, hidden}); return o; } @@ -140,6 +140,10 @@ void CommandLineParser::PrintHelp(const char* filename) printf("Usage: %s [options] %s\n\n", basename, positional_args.toUtf8().constData()); foreach (const KnownOption& o, options_) { + if (o.hidden) { + continue; + } + QString all_args; for (int i=0; i