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
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/common/CMakeLists.txt b/app/common/CMakeLists.txt
index 7f4d1fd76..83880359c 100644
--- a/app/common/CMakeLists.txt
+++ b/app/common/CMakeLists.txt
@@ -38,6 +38,7 @@ set(OLIVE_SOURCES
common/functiontimer.h
common/lerp.h
common/memorypool.cpp
+ common/otioutils.h
common/memorypool.h
common/ocioutils.cpp
common/ocioutils.h
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.
+
+***/
+
+#ifndef OTIOUTILS_H
+#define OTIOUTILS_H
+
+#ifdef USE_OTIO
+#include
+namespace OTIO = opentimelineio::OPENTIMELINEIO_VERSION;
+#endif
+
+#endif // OTIOUTILS
diff --git a/app/common/rational.cpp b/app/common/rational.cpp
index fa4bfd133..62c9af3e0 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 0d548523b..95c369d1b 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/core.cpp b/app/core.cpp
index c54283f31..6b0cebd5c 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()
@@ -1496,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/crashhandler/crashhandler.cpp b/app/dialog/crashhandler/crashhandler.cpp
index 3f31748de..a09ebcc43 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;
}
}
@@ -201,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);
@@ -233,24 +243,41 @@ 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) {
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;
}
// 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"));
@@ -259,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;
}
@@ -278,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();
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/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"));
diff --git a/app/main.cpp b/app/main.cpp
index 4043a53d3..8d241e1f0 100644
--- a/app/main.cpp
+++ b/app/main.cpp
@@ -76,32 +76,51 @@ int main(int argc, char *argv[])
CommandLineParser parser;
- const CommandLineParser::Option* help_option =
+ // Our options
+ auto help_option =
parser.AddOption({QStringLiteral("h"), QStringLiteral("-help")},
QCoreApplication::translate("main", "Show this help text"));
- const CommandLineParser::Option* version_option =
+ auto version_option =
parser.AddOption({QStringLiteral("v"), QStringLiteral("-version")},
QCoreApplication::translate("main", "Show application version"));
- const CommandLineParser::Option* fullscreen_option =
+ auto fullscreen_option =
parser.AddOption({QStringLiteral("f"), QStringLiteral("-fullscreen")},
QCoreApplication::translate("main", "Start in full-screen mode"));
- const CommandLineParser::Option* export_option =
+ auto export_option =
parser.AddOption({QStringLiteral("x"), QStringLiteral("-export")},
QCoreApplication::translate("main", "Export only (No GUI)"));
- const CommandLineParser::Option* ts_option =
+ auto ts_option =
parser.AddOption({QStringLiteral("-ts")},
QCoreApplication::translate("main", "Override language with file"),
true,
QCoreApplication::translate("main", "qm-file"));
- const CommandLineParser::PositionalArgument* project_argument =
+ auto project_argument =
parser.AddPositionalArgument(QStringLiteral("project"),
QCoreApplication::translate("main", "Project to open on startup"));
+ // Qt options re-implemented (add to this as necessary)
+ //
+ // Because we don't use QCommandLineParser, we must filter out Qt's arguments ourselves. Here,
+ // we create them so they're recognized, but never use and also hide them in the "help" text.
+ parser.AddOption({QStringLiteral("platform")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("platformpluginpath")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("platformtheme")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("plugin")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("qmljsdebugger")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("qwindowgeometry")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("qwindowicon")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("qwindowtitle")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("reverse")}, QString(), false, QString(), true);
+ parser.AddOption({QStringLiteral("session")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("style")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("stylesheet")}, QString(), true, QString(), true);
+ parser.AddOption({QStringLiteral("widgetcount")}, QString(), false, QString(), true);
+
parser.Process(argc, argv);
if (help_option->IsSet()) {
diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp
index 3d1194d80..ad08202bc 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),
@@ -51,10 +52,14 @@ 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);
+ 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 +82,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 +98,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 +129,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,22 +138,23 @@ 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 = speed();
- 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();
+ 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
@@ -151,21 +164,22 @@ 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
- if (IsInputKeyframing(kSpeedInput) || IsInputConnected(kSpeedInput)) {
- // FIXME: We'll need to calculate the speed hoo boy
- } else {
- double speed_value = GetStandardValue(kSpeedInput).toDouble();
+ if (reverse()) {
+ sequence_time = length() - sequence_time;
+ }
- 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);
- }
+ 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
+ sequence_time = 0;
+ } else if (!qFuzzyCompare(speed_value, 1.0)) {
+ // Multiply time
+ sequence_time = rational::fromDouble(sequence_time.toDouble() / speed_value);
}
return sequence_time;
@@ -208,6 +222,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/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/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/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
*/
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/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/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/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/node.cpp b/app/node/node.cpp
index 65dbe09f6..c0678db5a 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();
@@ -504,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 {
@@ -692,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
@@ -2285,12 +2296,15 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo()
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);
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/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/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 1642d1e73..8344cca0d 100644
--- a/app/node/project/footage/footage.cpp
+++ b/app/node/project/footage/footage.cpp
@@ -48,6 +48,9 @@ Footage::Footage(const QString &filename) :
Clear();
set_filename(filename);
+
+ SetCacheTextures(true);
+ SetViewerCacheEnabled(false);
}
void Footage::Retranslate()
@@ -298,66 +301,27 @@ 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);
+ // 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());
@@ -373,10 +337,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));
}
}
}
@@ -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;
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/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);
}
}
}
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/previewautocacher.cpp b/app/render/previewautocacher.cpp
index 6189521fa..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());
@@ -370,6 +370,8 @@ void PreviewAutoCacher::ClearHashQueue(bool wait)
for (auto it=copy.cbegin(); it!=copy.cend(); it++) {
(*it)->waitForFinished();
}
+
+ hash_tasks_.clear();
}
}
@@ -386,12 +388,21 @@ 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();
}
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;
@@ -411,6 +422,8 @@ void PreviewAutoCacher::ClearAudioQueue(bool wait)
for (auto it=copy.cbegin(); it!=copy.cend(); it++) {
it.key()->WaitForFinished();
}
+
+ audio_tasks_.clear();
}
}
@@ -427,6 +440,8 @@ void PreviewAutoCacher::ClearVideoDownloadQueue(bool wait)
for (auto it=copy.cbegin(); it!=copy.cend(); it++) {
it.key()->WaitForFinished();
}
+
+ video_download_tasks_.clear();
}
}
diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp
index 73449d9ea..3ea8c5102 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();
@@ -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/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/render/renderprocessor.cpp b/app/render/renderprocessor.cpp
index 948e78fe0..79c6a5d43 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:
@@ -226,19 +226,18 @@ NodeValueTable RenderProcessor::GenerateBlockTable(const Track *track, const Tim
continue;
}
- // 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);
+ }
+
+ if (b->GetStandardValue(Block::kReverseInput).toBool()) {
+ samples_from_this_block->reverse();
}
int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count());
@@ -274,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
@@ -534,34 +538,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;
diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp
index 536e47d2e..703298889 100644
--- a/app/task/precache/precachetask.cpp
+++ b/app/task/precache/precachetask.cpp
@@ -27,28 +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());
- // 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 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(footage_, 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/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);
diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp
index 240b19a69..6e84c7745 100644
--- a/app/task/project/loadotio/loadotio.cpp
+++ b/app/task/project/loadotio/loadotio.cpp
@@ -27,17 +27,17 @@
#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/project/folder/folder.h"
#include "node/project/footage/footage.h"
#include "node/project/sequence/sequence.h"
#include "widget/timelinewidget/timelineundo.h"
-#define OTIO opentimelineio::v1_0
-
namespace olive {
LoadOTIOTask::LoadOTIOTask(const QString& s) :
@@ -82,13 +82,16 @@ bool LoadOTIOTask::Run()
QMap imported_footage;
foreach (auto timeline, timelines) {
+ // Create sequence
Sequence* sequence = new Sequence();
sequence->SetLabel(QString::fromStdString(timeline->name()));
- sequence->setParent(project_->root());
+ sequence->setParent(project_);
+ FolderAddChild(project_->root(), sequence).redo();
// FIXME: As far as I know, OTIO doesn't store video/audio parameters?
sequence->set_default_parameters();
+ // Iterate through tracks
for (auto c : timeline->tracks()->children()) {
auto otio_track = static_cast(c.value);
@@ -105,6 +108,7 @@ bool LoadOTIOTask::Run()
type = Track::kAudio;
}
+ // Create track
TimelineAddTrackCommand t(sequence->track_list(type));
t.redo();
track = t.track();
@@ -120,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;
@@ -134,27 +141,67 @@ 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->setParent(project_);
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);
- block->setParent(sequence);
track->AppendBlock(block);
+ 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);
+ Node::ConnectEdge(block, NodeInput(previous_transition_block, TransitionBlock::kInBlockInput));
+ 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) {
+ Node::ConnectEdge(previous_block, NodeInput(transition_block, TransitionBlock::kOutBlockInput));
+
+ // 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;
+ }
+
+ // 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());
@@ -166,7 +213,7 @@ bool LoadOTIOTask::Run()
} else {
probed_item = new Footage(footage_url);
imported_footage.insert(footage_url, probed_item);
- probed_item->setParent(project_->root());
+ probed_item->setParent(project_);
}
Track::Reference reference;
@@ -194,4 +241,4 @@ bool LoadOTIOTask::Run()
}
-#endif
+#endif // USE_OTIO
diff --git a/app/task/project/loadotio/loadotio.h b/app/task/project/loadotio/loadotio.h
index 55579fcd7..702d8e73a 100644
--- a/app/task/project/loadotio/loadotio.h
+++ b/app/task/project/loadotio/loadotio.h
@@ -23,6 +23,7 @@
#ifdef USE_OTIO
+#include "common/otioutils.h"
#include "node/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 791821772..a683b058a 100644
--- a/app/task/project/saveotio/saveotio.cpp
+++ b/app/task/project/saveotio/saveotio.cpp
@@ -26,6 +26,7 @@
#include
#include
#include
+#include
#include
#include "node/block/transition/transition.h"
@@ -48,7 +49,7 @@ bool SaveOTIOTask::Run()
return false;
}
- std::vector serialized;
+ std::vector serialized;
foreach (Sequence* seq, sequences) {
auto otio_timeline = SerializeTimeline(seq);
@@ -69,7 +70,7 @@ bool SaveOTIOTask::Run()
}
}
- opentimelineio::v1_0::ErrorStatus es;
+ OTIO::ErrorStatus es;
if (serialized.size() == 1) {
// Serialize timeline on its own
@@ -78,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();
@@ -88,12 +89,16 @@ bool SaveOTIOTask::Run()
}
}
- return (es == opentimelineio::v1_0::ErrorStatus::OK);
+ return (es == OTIO::ErrorStatus::OK);
}
-opentimelineio::v1_0::Timeline *SaveOTIOTask::SerializeTimeline(Sequence *sequence)
+OTIO::Timeline *SaveOTIOTask::SerializeTimeline(Sequence *sequence)
{
- auto otio_timeline = new opentimelineio::v1_0::Timeline(sequence->GetLabel().toStdString());
+ auto otio_timeline = new OTIO::Timeline(sequence->GetLabel().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->track_list(Track::kVideo), otio_timeline)
|| !SerializeTrackList(sequence->track_list(Track::kAudio), otio_timeline)) {
@@ -104,11 +109,11 @@ opentimelineio::v1_0::Timeline *SaveOTIOTask::SerializeTimeline(Sequence *sequen
return otio_timeline;
}
-opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(Track *track)
+OTIO::Track *SaveOTIOTask::SerializeTrack(Track *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->type()) {
case Track::kVideo:
@@ -123,19 +128,19 @@ opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(Track *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(),
- block->length().toRationalTime()));
+ otio_clip->set_source_range(OTIO::TimeRange(block->in().toRationalTime(),
+ block->length().toRationalTime()));
QVector media_nodes = block->FindInputNodes();
if (!media_nodes.isEmpty()) {
- auto media_ref = new opentimelineio::v1_0::ExternalReference(media_nodes.first()->filename().toStdString());
+ auto media_ref = new OTIO::ExternalReference(media_nodes.first()->filename().toStdString());
otio_clip->set_media_reference(media_ref);
}
@@ -144,22 +149,22 @@ opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(Track *track)
}
case Block::kGap:
{
- otio_block = new opentimelineio::v1_0::Gap(
- opentimelineio::v1_0::TimeRange(block->in().toRationalTime(), block->length().toRationalTime()),
- block->GetLabel().toStdString()
- );
+ otio_block = new OTIO::Gap(OTIO::TimeRange(block->in().toRationalTime(),
+ block->length().toRationalTime()),
+ block->GetLabel().toStdString()
+ );
break;
}
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;
}
}
@@ -171,7 +176,7 @@ opentimelineio::v1_0::Track *SaveOTIOTask::SerializeTrack(Track *track)
otio_track->append_child(otio_block, &es);
- if (es != opentimelineio::v1_0::ErrorStatus::OK) {
+ if (es != OTIO::ErrorStatus::OK) {
goto fail;
}
}
@@ -184,9 +189,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 (Track* track, list->GetTracks()) {
auto otio_track = SerializeTrack(track);
@@ -197,7 +202,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;
}
@@ -208,4 +213,4 @@ bool SaveOTIOTask::SerializeTrackList(TrackList *list, opentimelineio::v1_0::Tim
}
-#endif
+#endif // USE_OTIO
diff --git a/app/task/project/saveotio/saveotio.h b/app/task/project/saveotio/saveotio.h
index 658849de6..076a0470d 100644
--- a/app/task/project/saveotio/saveotio.h
+++ b/app/task/project/saveotio/saveotio.h
@@ -26,6 +26,7 @@
#include
#include
+#include "common/otioutils.h"
#include "node/project/project.h"
#include "task/task.h"
@@ -41,11 +42,11 @@ protected:
virtual bool Run() override;
private:
- opentimelineio::v1_0::Timeline* SerializeTimeline(Sequence* sequence);
+ OTIO::Timeline* SerializeTimeline(Sequence* sequence);
- opentimelineio::v1_0::Track* SerializeTrack(Track* track);
+ OTIO::Track* SerializeTrack(Track* track);
- bool SerializeTrackList(TrackList* list, opentimelineio::v1_0::Timeline *otio_timeline);
+ bool SerializeTrackList(TrackList* list, OTIO::Timeline *otio_timeline);
Project* project_;
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
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;
diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp
index 98f835529..bbb9b7fb2 100644
--- a/app/widget/nodeparamview/nodeparamview.cpp
+++ b/app/widget/nodeparamview/nodeparamview.cpp
@@ -262,7 +262,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()
@@ -310,8 +310,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 1d9b16f15..b1c3e3dbf 100644
--- a/app/widget/nodeparamview/nodeparamviewitem.cpp
+++ b/app/widget/nodeparamview/nodeparamviewitem.cpp
@@ -36,8 +36,13 @@ 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) :
- QDockWidget(parent),
+ super(parent),
node_(node),
highlighted_(false)
{
@@ -97,12 +102,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_) {
@@ -113,6 +118,13 @@ void NodeParamViewItem::paintEvent(QPaintEvent *event)
}
}
+void NodeParamViewItem::moveEvent(QMoveEvent *event)
+{
+ super::moveEvent(event);
+
+ emit Moved();
+}
+
void NodeParamViewItem::Retranslate()
{
node_->Retranslate();
@@ -257,6 +269,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();
@@ -299,23 +315,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
@@ -414,6 +431,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()));
@@ -510,12 +537,19 @@ void NodeParamViewItemBody::ToggleArrayExpanded()
}
}
-void NodeParamViewItemBody::SetTimebase(const rational& timebase) {
+void NodeParamViewItemBody::SetTimebase(const rational& timebase)
+{
foreach (const InputUI& ui_obj, input_ui_map_) {
ui_obj.widget_bridge->SetTimebase(timebase);
}
}
+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 0b4729bd0..813da7221 100644
--- a/app/widget/nodeparamview/nodeparamviewitem.h
+++ b/app/widget/nodeparamview/nodeparamviewitem.h
@@ -99,6 +99,8 @@ private:
void UpdateUIForEdgeConnection(const NodeInput &input);
+ void PlaceWidgetsFromBridge(QGridLayout *layout, NodeParamViewWidgetBridge* bridge, int row);
+
struct InputUI {
InputUI();
@@ -106,6 +108,8 @@ private:
NodeParamViewWidgetBridge* widget_bridge;
NodeParamViewConnectedLabel* connected_label;
NodeParamViewKeyframeControl* key_control;
+ QGridLayout* layout;
+ int row;
NodeParamViewArrayButton* array_insert_btn;
NodeParamViewArrayButton* array_remove_btn;
@@ -134,6 +138,8 @@ private:
static const int kArrayInsertColumn;
static const int kArrayRemoveColumn;
+ static const int kWidgetStartColumn;
+
private slots:
void EdgeChanged(const NodeOutput &output, const NodeInput &input);
@@ -149,6 +155,8 @@ private slots:
void ToggleArrayExpanded();
+ void ReplaceWidgets(const NodeInput& input);
+
};
class NodeParamViewItem : public QDockWidget
@@ -192,11 +200,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_;
diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp
index c2a5d8c8d..50af8ca33 100644
--- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp
+++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp
@@ -50,6 +50,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)
@@ -212,14 +213,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));
@@ -767,6 +779,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 d985eb52d..cba43e1a6 100644
--- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h
+++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h
@@ -54,6 +54,8 @@ public:
signals:
void ArrayWidgetDoubleClicked();
+ void WidgetsRecreated(const NodeInput& input);
+
private:
void CreateWidgets();
@@ -87,6 +89,8 @@ private slots:
void PropertyChanged(const QString &input, const QString& key, const QVariant& value);
+ void InputDataTypeChanged(const QString& input, NodeValue::Type type);
+
};
}
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 {
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);
+ }
}
}
}
diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp
index 581145055..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());
@@ -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_) {
diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp
index cf0113ce2..8a8a229d7 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)
@@ -801,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());
@@ -854,6 +863,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_;
diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp
index ed69336c4..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());
@@ -331,21 +333,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) {
@@ -407,6 +408,8 @@ void MainWindow::closeEvent(QCloseEvent *e)
PanelManager::instance()->DeleteAllPanels();
+ SaveCustomShortcuts();
+
QMainWindow::closeEvent(e);
}
@@ -523,11 +526,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();
}
@@ -550,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);
@@ -643,9 +745,9 @@ void MainWindow::showEvent(QShowEvent *e)
if (!strcmp(vendor, "nouveau")) {
QMetaObject::invokeMethod(this, "ShowNouveauWarning", Qt::QueuedConnection);
}
+#endif
first_show_ = false;
-#endif
}
}
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