diff --git a/.appveyor/build.bat b/.appveyor/build.bat index 59eca0e85..e2c6b553f 100644 --- a/.appveyor/build.bat +++ b/.appveyor/build.bat @@ -34,10 +34,13 @@ REM Add Qt and FFmpeg directory to path set PATH=%PATH%;C:\Qt\5.13.2\msvc2017_64\bin;%APPVEYOR_BUILD_FOLDER%\%FFMPEG_VER%-dev REM Run cmake -cmake -G "NMake Makefiles" . -DCMAKE_TOOLCHAIN_FILE=c:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo +cmake -G "Ninja" . -DCMAKE_TOOLCHAIN_FILE=c:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -REM Build with JOM -C:\Qt\Tools\QtCreator\bin\jom.exe || exit /B 1 +REM Build with Ninja +ninja.exe || exit /B 1 + +REM If this is a pull request, no further packaging/deploying needs to be done +if NOT "%APPVEYOR_PULL_REQUEST_NUMBER%" == "" goto end REM Start building package mkdir olive-editor diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f74cc0883..e9e35c5a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,19 @@ # Contributing to Olive +Thank you for your interest in contributing to Olive! In order to keep the code as readable and maintainable as possible, code submitted should abide by the following standards: + ### Standards When contributing to Olive, it's recommended to use the following rules: -* [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) -* 120 column limit \ No newline at end of file +* The code style generally follows the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) including, but not limited to: + * Indentation is 2 spaces wide, spaces only (no tabs) + * `lowercase_underscored_variable_names` + * `lowercase_underscored_functions()` or `SentenceCaseFunctions()` + * `class SentenceCaseClassesAndStructs {}` + * `kSentenceCaseConstants` prepended with a lowercase `k` + * `UPPERCASE_UNDERSCORED_MACROS` for variables or same style as functions for macro functions + * `class_member_variables_` end with a `_` +* 100 column limit (where it doesn't impair readability) +* Unix line endings (only LF no CRLF) +* Javadoc documentation where appropriate diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 24f7d9116..b22b69ced 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -75,6 +75,8 @@ if(APPLE) MACOSX_BUNDLE_ICON_FILE olive.icns RESOURCE "${OLIVE_ICON}" ) + + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9") endif() # Set compiler definitions @@ -107,6 +109,14 @@ else() ) endif() +if(UNIX AND NOT APPLE) + target_compile_options( + ${OLIVE_TARGET} + PRIVATE + -rdynamic + ) +endif() + # Set include directories target_include_directories( ${OLIVE_TARGET} diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index 26ef71989..c9cd17350 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -40,6 +40,7 @@ EncodingParams::EncodingParams() : video_bit_rate_(0), video_max_bit_rate_(0), video_buffer_size_(0), + video_threads_(0), audio_enabled_(false) { } @@ -63,26 +64,31 @@ void EncodingParams::EnableAudio(const AudioRenderingParams &audio_params, const audio_codec_ = acodec; } -void EncodingParams::SetVideoOption(const QString &key, const QString &value) +void EncodingParams::set_video_option(const QString &key, const QString &value) { video_opts_.insert(key, value); } -void EncodingParams::SetVideoBitRate(const int64_t &rate) +void EncodingParams::set_video_bit_rate(const int64_t &rate) { video_bit_rate_ = rate; } -void EncodingParams::SetVideoMaxBitRate(const int64_t &rate) +void EncodingParams::set_video_max_bit_rate(const int64_t &rate) { video_max_bit_rate_ = rate; } -void EncodingParams::SetVideoBufferSize(const int64_t &sz) +void EncodingParams::set_video_buffer_size(const int64_t &sz) { video_buffer_size_ = sz; } +void EncodingParams::set_video_threads(const int &threads) +{ + video_threads_ = threads; +} + const QString &EncodingParams::filename() const { return filename_; @@ -123,6 +129,11 @@ const int64_t &EncodingParams::video_buffer_size() const return video_buffer_size_; } +const int &EncodingParams::video_threads() const +{ + return video_threads_; +} + bool EncodingParams::audio_enabled() const { return audio_enabled_; diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 198697991..05ce58760 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -43,10 +43,11 @@ public: void EnableVideo(const VideoRenderingParams& video_params, const QString& vcodec); void EnableAudio(const AudioRenderingParams& audio_params, const QString& acodec); - void SetVideoOption(const QString& key, const QString& value); - void SetVideoBitRate(const int64_t& rate); - void SetVideoMaxBitRate(const int64_t& rate); - void SetVideoBufferSize(const int64_t& sz); + void set_video_option(const QString& key, const QString& value); + void set_video_bit_rate(const int64_t& rate); + void set_video_max_bit_rate(const int64_t& rate); + void set_video_buffer_size(const int64_t& sz); + void set_video_threads(const int& threads); const QString& filename() const; @@ -57,6 +58,7 @@ public: const int64_t& video_bit_rate() const; const int64_t& video_max_bit_rate() const; const int64_t& video_buffer_size() const; + const int& video_threads() const; bool audio_enabled() const; const QString& audio_codec() const; @@ -75,6 +77,7 @@ private: int64_t video_bit_rate_; int64_t video_max_bit_rate_; int64_t video_buffer_size_; + int video_threads_; bool audio_enabled_; QString audio_codec_; diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index 24c753acd..86bbf9309 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -468,7 +468,14 @@ bool FFmpegEncoder::SetupCodecContext(AVStream* stream, AVCodecContext* codec_ct } AVDictionary* codec_opts = nullptr; - av_dict_set(&codec_opts, "threads", "auto", 0); + + // Set thread count + if (params().video_threads() == 0) { + av_dict_set(&codec_opts, "threads", "auto", 0); + } else { + QString thread_val = QString::number(params().video_threads()); + av_dict_set(&codec_opts, "threads", thread_val.toUtf8(), 0); + } // Try to open encoder error_code = avcodec_open2(codec_ctx, codec, &codec_opts); diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index 813011f6a..52653df05 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -192,8 +192,7 @@ FramePtr OIIODecoder::RetrieveVideo(const rational &timecode, const int& divider if (divider == 1) { - // Just a simple copy - buffer_->get_pixels(OIIO::ROI(), buffer_->spec().format, frame->data(), OIIO::AutoStride, frame->linesize_bytes()); + BufferToFrame(buffer_, frame); } else { @@ -204,8 +203,7 @@ FramePtr OIIODecoder::RetrieveVideo(const rational &timecode, const int& divider qWarning() << "OIIO resize failed"; } - // Just a simple copy - dst.get_pixels(OIIO::ROI(), dst.spec().format, frame->data(), OIIO::AutoStride, frame->linesize_bytes()); + BufferToFrame(&dst, frame); } @@ -233,6 +231,34 @@ QString OIIODecoder::GetIndexFilename() return QString(); } +void OIIODecoder::BufferToFrame(OIIO::ImageBuf *buf, FramePtr frame) +{ +#if OIIO_VERSION < 20112 + // + // Workaround for OIIO bug that ignores destination stride in versions OLDER than 2.1.12 + // + // See more: https://github.com/OpenImageIO/oiio/pull/2487 + // + for (int i=0;ispec().height;i++) { + int width_in_bytes = frame->width() * PixelFormat::BytesPerPixel(frame->format()); + + memcpy(frame->data() + i * frame->linesize_bytes(), +#if OIIO_VERSION < 10903 + reinterpret_cast(buf->localpixels()) + i * width_in_bytes, +#else + reinterpret_cast(buf->localpixels()) + i * buf->scanline_stride(), +#endif + width_in_bytes); + } +#else + buf->get_pixels(OIIO::ROI(), + buf->spec().format, + frame->data(), + OIIO::AutoStride, + frame->linesize_bytes()); +#endif +} + bool OIIODecoder::FileTypeIsSupported(const QString& fn) { // We prioritize OIIO over FFmpeg to pick up still images more effectively, but some OIIO decoders (notably OpenJPEG) diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index 787596691..249a1a346 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -48,12 +48,15 @@ public: virtual QString GetIndexFilename() override; + static void BufferToFrame(OIIO::ImageBuf* buf, FramePtr frame); + private: #if OIIO_VERSION < 10903 OIIO::ImageInput* image_; #else std::unique_ptr image_; #endif + static bool FileTypeIsSupported(const QString& fn); static int GetImageSequenceDigitCount(const QString& filename); diff --git a/app/common/crashhandler.cpp b/app/common/crashhandler.cpp index b8ac83a0c..615541493 100644 --- a/app/common/crashhandler.cpp +++ b/app/common/crashhandler.cpp @@ -41,14 +41,15 @@ OLIVE_NAMESPACE_ENTER -void crash_handler(int sig) { +void crash_handler(int sig) +{ QString log_path = QDir(FileFunctions::GetTempFilePath()).filePath(QStringLiteral("olive_crash")); QFile output(log_path); output.open(QFile::WriteOnly); QTextStream ostream(&output); - ostream << "Signal: " << sig << "\n\n"; + ostream << "Version: " << GITHASH << "\nSignal: " << sig << "\n\n"; #if defined(Q_OS_WINDOWS) // Use Windows stackwalk API diff --git a/app/common/xmlutils.cpp b/app/common/xmlutils.cpp index 0b39e6d32..9bea8f3b0 100644 --- a/app/common/xmlutils.cpp +++ b/app/common/xmlutils.cpp @@ -26,15 +26,28 @@ OLIVE_NAMESPACE_ENTER -Node* XMLLoadNode(QXmlStreamReader* reader) { +Node* XMLLoadNode(QXmlStreamReader* reader) +{ QString node_id; quintptr node_ptr = 0; + QPointF node_pos; + QString node_label; XMLAttributeLoop(reader, attr) { if (attr.name() == QStringLiteral("id")) { node_id = attr.value().toString(); } else if (attr.name() == QStringLiteral("ptr")) { node_ptr = attr.value().toULongLong(); + } else if (attr.name() == QStringLiteral("pos")) { + QStringList pos = attr.value().toString().split(':'); + + // Protection in case this file has been messed with + if (pos.size() == 2) { + node_pos.setX(pos.at(0).toDouble()); + node_pos.setY(pos.at(1).toDouble()); + } + } else if (attr.name() == QStringLiteral("label")) { + node_label = attr.value().toString(); } } @@ -47,6 +60,8 @@ Node* XMLLoadNode(QXmlStreamReader* reader) { if (node) { node->setProperty("xml_ptr", node_ptr); + node->SetPosition(node_pos); + node->SetLabel(node_label); } else { qWarning() << "Failed to load" << node_id << "- no node with that ID is installed"; } diff --git a/app/core.cpp b/app/core.cpp index f41294ff4..15699cb5c 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -536,9 +536,9 @@ void Core::DeclareTypesForQt() qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); + qRegisterMetaType(); } void Core::StartGUI(bool full_screen) diff --git a/app/core.h b/app/core.h index 20ef0a4ea..8c6043e8a 100644 --- a/app/core.h +++ b/app/core.h @@ -477,12 +477,12 @@ private: private slots: void SaveAutorecovery(); - void ProjectSaveSucceeded(ProjectPtr p); + void ProjectSaveSucceeded(OLIVE_NAMESPACE::ProjectPtr p); /** * @brief Adds a project to the "open projects" list */ - void AddOpenProject(ProjectPtr p); + void AddOpenProject(OLIVE_NAMESPACE::ProjectPtr p); void ImportTaskComplete(QUndoCommand* command); diff --git a/app/dialog/export/CMakeLists.txt b/app/dialog/export/CMakeLists.txt index 4a592ff21..b1ef3ae2c 100644 --- a/app/dialog/export/CMakeLists.txt +++ b/app/dialog/export/CMakeLists.txt @@ -20,6 +20,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} dialog/export/export.h dialog/export/export.cpp + dialog/export/exportadvancedvideodialog.h + dialog/export/exportadvancedvideodialog.cpp dialog/export/exportaudiotab.h dialog/export/exportaudiotab.cpp dialog/export/exportcodec.h diff --git a/app/dialog/export/codec/h264section.cpp b/app/dialog/export/codec/h264section.cpp index 8d64647a2..42833f665 100644 --- a/app/dialog/export/codec/h264section.cpp +++ b/app/dialog/export/codec/h264section.cpp @@ -78,7 +78,7 @@ void H264Section::AddOpts(EncodingParams *params) if (method == kConstantRateFactor) { // Simply set CRF value - params->SetVideoOption(QStringLiteral("crf"), QString::number(crf_section_->GetValue())); + params->set_video_option(QStringLiteral("crf"), QString::number(crf_section_->GetValue())); } else { @@ -95,11 +95,11 @@ void H264Section::AddOpts(EncodingParams *params) } // Disable CRF encoding - params->SetVideoOption(QStringLiteral("crf"), QStringLiteral("-1")); + params->set_video_option(QStringLiteral("crf"), QStringLiteral("-1")); - params->SetVideoBitRate(target_rate); - params->SetVideoMaxBitRate(max_rate); - params->SetVideoBufferSize(2000000); + params->set_video_bit_rate(target_rate); + params->set_video_max_bit_rate(max_rate); + params->set_video_buffer_size(2000000); } } diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index 81b7f845b..23480684b 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -238,10 +238,13 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : void ExportDialog::accept() { if (!video_enabled_->isChecked() && !audio_enabled_->isChecked()) { - QMessageBox::critical(this, - tr("Invalid parameters"), - tr("Both video and audio are disabled. There's nothing to export."), - QMessageBox::Ok); + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Invalid parameters")); + b.setText(tr("Both video and audio are disabled. There's nothing to export.")); + b.addButton(QMessageBox::Ok); + b.exec(); return; } @@ -251,10 +254,16 @@ void ExportDialog::accept() // If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export. if (!filename_edit_->text().endsWith(necessary_ext, Qt::CaseInsensitive)) { - if (QMessageBox::warning(this, - tr("Invalid filename"), - tr("The filename must contain the extension \".%1\". Would you like to append it automatically?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + QMessageBox b(this); + b.setIcon(QMessageBox::Warning); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Invalid filename")); + b.setText(tr("The filename must contain the extension \".%1\". Would you like to append it " + "automatically?")); + b.addButton(QMessageBox::Yes); + b.addButton(QMessageBox::No); + + if (b.exec() == QMessageBox::Yes) { filename_edit_->setText(filename_edit_->text().append(necessary_ext)); } else { return; @@ -267,20 +276,45 @@ void ExportDialog::accept() // If the directory does not exist, try to create it if (!QDir(file_info.path()).mkpath(QStringLiteral("."))) { - QMessageBox::critical(this, - tr("Failed to create output directory"), - tr("The intended output directory doesn't exist and Olive couldn't create it. Please choose a different filename."), - QMessageBox::Ok); + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Failed to create output directory")); + b.setText(tr("The intended output directory doesn't exist and Olive couldn't create it. " + "Please choose a different filename.")); + b.addButton(QMessageBox::Ok); + b.exec(); return; } // Validate if the file exists and whether the user wishes to overwrite it - if (file_info.exists() - && QMessageBox::warning(this, - tr("Confirm Overwrite"), - tr("The file \"%1\" already exists. Do you want to overwrite it?").arg(filename_edit_->text()), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { - return; + if (file_info.exists()) { + QMessageBox b(this); + b.setIcon(QMessageBox::Warning); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Confirm Overwrite")); + b.setText(tr("The file \"%1\" already exists. Do you want to overwrite it?") + .arg(filename_edit_->text())); + b.addButton(QMessageBox::Yes); + b.addButton(QMessageBox::No); + + if (b.exec() == QMessageBox::No) { + return; + } + } + + // Validate video resolution + if (video_enabled_->isChecked()) { + if (video_tab_->width_slider()->GetValue() % 2 != 0 + || video_tab_->height_slider()->GetValue() % 2 != 0) { + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Invalid parameters")); + b.setText(tr("Width and height must be multiples of 2.")); + b.exec(); + return; + } } // Set up export parameters @@ -307,10 +341,15 @@ void ExportDialog::accept() void ExportDialog::closeEvent(QCloseEvent *e) { if (exporter_) { - if (QMessageBox::question(this, - tr("Still Exporting"), - tr("This sequence is still being exported. Do you wish to cancel it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + QMessageBox b(this); + b.setIcon(QMessageBox::Question); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Still Exporting")); + b.setText(tr("This sequence is still being exported. Do you wish to cancel it?")); + b.addButton(QMessageBox::Yes); + b.addButton(QMessageBox::No); + + if (b.exec() == QMessageBox::Yes) { CancelExport(); } else { e->ignore(); @@ -378,10 +417,27 @@ void ExportDialog::ResolutionChanged() if (video_tab_->maintain_aspect_checkbox()->isChecked()) { // Keep aspect ratio maintained if (sender() == video_tab_->height_slider()) { - video_tab_->width_slider()->SetValue(qRound(static_cast(video_tab_->height_slider()->GetValue()) * video_aspect_ratio_)); + + // Convert height to float + double new_width = video_tab_->height_slider()->GetValue(); + + // Generate width from aspect ratio + new_width *= video_aspect_ratio_; + + // Align to even number and set + video_tab_->width_slider()->SetValue(AlignEvenNumber(new_width)); + } else { - // This catches both the width slider changing and the maintain aspect ratio checkbox changing - video_tab_->height_slider()->SetValue(qRound(static_cast(video_tab_->width_slider()->GetValue()) / video_aspect_ratio_)); + + // Convert width to float + double new_height = video_tab_->width_slider()->GetValue(); + + // Generate height from aspect ratio + new_height /= video_aspect_ratio_; + + // Align to even number and set + video_tab_->height_slider()->SetValue(AlignEvenNumber(new_height)); + } } @@ -500,8 +556,15 @@ void ExportDialog::SetUIElementsEnabled(bool enabled) preferences_area_->setEnabled(enabled); buttons_->setEnabled(enabled); + progress_bar_->setEnabled(!enabled); export_cancel_btn_->setEnabled(!enabled); elapsed_label_->setEnabled(!enabled); + remaining_label_->setEnabled(!enabled); +} + +int ExportDialog::AlignEvenNumber(double d) +{ + return qCeil(d * 0.5) * 2; } ExportParams ExportDialog::GenerateParams() const @@ -531,6 +594,8 @@ ExportParams ExportDialog::GenerateParams() const params.EnableVideo(video_render_params, video_codec.id()); + params.set_video_threads(video_tab_->threads()); + video_tab_->GetCodecSection()->AddOpts(¶ms); params.set_color_transform(video_tab_->CurrentOCIOColorSpace()); @@ -609,10 +674,13 @@ void ExportDialog::ExporterIsDone() progress_timer_.stop(); if (exporter_->GetExportStatus()) { - QMessageBox::information(this, - tr("Export Status"), - tr("Export completed successfully."), - QMessageBox::Ok); + QMessageBox b(this); + b.setIcon(QMessageBox::Information); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Export Status")); + b.setText(tr("Export completed successfully.")); + b.addButton(QMessageBox::Ok); + b.exec(); QDialog::accept(); } else { @@ -621,16 +689,18 @@ void ExportDialog::ExporterIsDone() Core::instance()->main_window()->SetTaskbarButtonState(TBPF_ERROR); #endif - QMessageBox::critical(this, - tr("Export Status"), - tr("Export failed: %1").arg(exporter_->GetExportError()), - QMessageBox::Ok); + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("Export Status")); + b.setText(tr("Export failed: %1").arg(exporter_->GetExportError())); + b.addButton(QMessageBox::Ok); + b.exec(); } SetUIElementsEnabled(true); } - exporter_->deleteLater(); exporter_ = nullptr; cancelled_ = false; diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index 82d610ced..25dd21d7d 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -55,6 +55,8 @@ private: void SetUIElementsEnabled(bool enabled); + static int AlignEvenNumber(double d); + ExportParams GenerateParams() const; static QString TimeToString(int64_t ms); diff --git a/app/dialog/export/exportadvancedvideodialog.cpp b/app/dialog/export/exportadvancedvideodialog.cpp new file mode 100644 index 000000000..bbaba2ee6 --- /dev/null +++ b/app/dialog/export/exportadvancedvideodialog.cpp @@ -0,0 +1,42 @@ +#include "exportadvancedvideodialog.h" + +#include +#include +#include + +OLIVE_NAMESPACE_ENTER + +ExportAdvancedVideoDialog::ExportAdvancedVideoDialog(QWidget *parent) : + QDialog(parent) +{ + setWindowTitle(tr("Advanced")); + + QGridLayout* layout = new QGridLayout(this); + + int row = 0; + + layout->addWidget(new QLabel(tr("Threads:")), row, 0); + + thread_slider_ = new IntegerSlider(); + thread_slider_->SetMinimum(0); + layout->addWidget(thread_slider_, row, 1); + + row++; + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &ExportAdvancedVideoDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ExportAdvancedVideoDialog::reject); + layout->addWidget(buttons, row, 0, 1, 2); +} + +int ExportAdvancedVideoDialog::threads() const +{ + return static_cast(thread_slider_->GetValue()); +} + +void ExportAdvancedVideoDialog::set_threads(int t) +{ + thread_slider_->SetValue(t); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/export/exportadvancedvideodialog.h b/app/dialog/export/exportadvancedvideodialog.h new file mode 100644 index 000000000..81b2c524f --- /dev/null +++ b/app/dialog/export/exportadvancedvideodialog.h @@ -0,0 +1,27 @@ +#ifndef EXPORTADVANCEDVIDEODIALOG_H +#define EXPORTADVANCEDVIDEODIALOG_H + +#include + +#include "render/backend/exportparams.h" +#include "widget/slider/integerslider.h" + +OLIVE_NAMESPACE_ENTER + +class ExportAdvancedVideoDialog : public QDialog +{ + Q_OBJECT +public: + ExportAdvancedVideoDialog(QWidget* parent = nullptr); + + int threads() const; + void set_threads(int t); + +private: + IntegerSlider* thread_slider_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // EXPORTADVANCEDVIDEODIALOG_H diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp index 352d86910..c6a5b1d98 100644 --- a/app/dialog/export/exportvideotab.cpp +++ b/app/dialog/export/exportvideotab.cpp @@ -24,8 +24,10 @@ #include #include #include +#include #include "core.h" +#include "exportadvancedvideodialog.h" #include "render/backend/exportparams.h" #include "render/colormanager.h" @@ -33,7 +35,8 @@ OLIVE_NAMESPACE_ENTER ExportVideoTab::ExportVideoTab(ColorManager* color_manager, QWidget *parent) : QWidget(parent), - color_manager_(color_manager) + color_manager_(color_manager), + threads_(0) { QVBoxLayout* outer_layout = new QVBoxLayout(this); @@ -106,6 +109,11 @@ H264Section *ExportVideoTab::h264_section() const return h264_section_; } +const int &ExportVideoTab::threads() const +{ + return threads_; +} + QWidget* ExportVideoTab::SetupResolutionSection() { int row = 0; @@ -118,6 +126,7 @@ QWidget* ExportVideoTab::SetupResolutionSection() layout->addWidget(new QLabel(tr("Width:")), row, 0); width_slider_ = new IntegerSlider(); + width_slider_->SetMinimum(1); layout->addWidget(width_slider_, row, 1); row++; @@ -125,6 +134,7 @@ QWidget* ExportVideoTab::SetupResolutionSection() layout->addWidget(new QLabel(tr("Height:")), row, 0); height_slider_ = new IntegerSlider(); + height_slider_->SetMinimum(1); layout->addWidget(height_slider_, row, 1); row++; @@ -196,6 +206,12 @@ QWidget *ExportVideoTab::SetupCodecSection() h264_section_ = new H264Section(); codec_stack_->addWidget(h264_section_); + row++; + + QPushButton* advanced_btn = new QPushButton(tr("Advanced")); + connect(advanced_btn, &QPushButton::clicked, this, &ExportVideoTab::OpenAdvancedDialog); + codec_layout->addWidget(advanced_btn, row, 1); + return codec_group; } @@ -204,4 +220,15 @@ void ExportVideoTab::MaintainAspectRatioChanged(bool val) scaling_method_combobox_->setEnabled(!val); } +void ExportVideoTab::OpenAdvancedDialog() +{ + ExportAdvancedVideoDialog d(this); + + d.set_threads(threads_); + + if (d.exec() == QDialog::Accepted) { + threads_ = d.threads(); + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/export/exportvideotab.h b/app/dialog/export/exportvideotab.h index 1332f14a5..7ad0c72d6 100644 --- a/app/dialog/export/exportvideotab.h +++ b/app/dialog/export/exportvideotab.h @@ -57,6 +57,8 @@ public: ImageSection* image_section() const; H264Section* h264_section() const; + const int& threads() const; + signals: void ColorSpaceChanged(const QString& colorspace); @@ -83,9 +85,13 @@ private: ColorManager* color_manager_; + int threads_; + private slots: void MaintainAspectRatioChanged(bool val); + void OpenAdvancedDialog(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.cpp b/app/dialog/preferences/tabs/preferencesaudiotab.cpp index af70cb1d2..dfe3536a7 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.cpp +++ b/app/dialog/preferences/tabs/preferencesaudiotab.cpp @@ -22,7 +22,6 @@ #include #include -#include #include "audio/audiomanager.h" #include "config/config.h" @@ -80,14 +79,14 @@ PreferencesAudioTab::PreferencesAudioTab() row++; - QPushButton* refresh_devices = new QPushButton(tr("Refresh Devices")); - audio_tab_layout->addWidget(refresh_devices, row, 1); + refresh_devices_btn_ = new QPushButton(tr("Refresh Devices")); + audio_tab_layout->addWidget(refresh_devices_btn_, row, 1); row++; RetrieveDeviceLists(); - connect(refresh_devices, &QPushButton::clicked, this, &PreferencesAudioTab::RefreshDevices); + 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); } @@ -151,6 +150,8 @@ void PreferencesAudioTab::RetrieveOutputList() AudioManager::instance()->IsRefreshingOutputs(), AudioManager::instance()->ListOutputDevices(), Config::Current()["AudioOutput"].toString()); + + UpdateRefreshButtonEnabled(); } void PreferencesAudioTab::RetrieveInputList() @@ -159,6 +160,8 @@ void PreferencesAudioTab::RetrieveInputList() AudioManager::instance()->IsRefreshingInputs(), AudioManager::instance()->ListInputDevices(), Config::Current()["AudioInput"].toString()); + + UpdateRefreshButtonEnabled(); } void PreferencesAudioTab::RetrieveDeviceLists() @@ -167,17 +170,22 @@ void PreferencesAudioTab::RetrieveDeviceLists() RetrieveInputList(); } +void PreferencesAudioTab::UpdateRefreshButtonEnabled() +{ + refresh_devices_btn_->setEnabled(audio_output_devices_->isEnabled() + && audio_input_devices_->isEnabled()); +} + void PreferencesAudioTab::PopulateComboBox(QComboBox *cb, bool still_refreshing, const QList &list, const QString& preferred) { cb->clear(); - cb->setEnabled(still_refreshing); + cb->setEnabled(!still_refreshing); if (still_refreshing) { cb->addItem(tr("Please wait...")); } else { bool found_preferred_device = false; - cb->setEnabled(true); // Add null default item cb->addItem(tr("Default"), QVariant()); diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.h b/app/dialog/preferences/tabs/preferencesaudiotab.h index f91445774..dcf9252b7 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.h +++ b/app/dialog/preferences/tabs/preferencesaudiotab.h @@ -23,6 +23,7 @@ #include #include +#include #include "preferencestab.h" @@ -57,6 +58,11 @@ private: */ QComboBox* recording_combobox_; + /** + * @brief Button that triggers a refresh of the available audio devices + */ + QPushButton* refresh_devices_btn_; + private slots: void RefreshDevices(); @@ -67,6 +73,8 @@ private slots: private: void RetrieveDeviceLists(); + void UpdateRefreshButtonEnabled(); + static void PopulateComboBox(QComboBox* cb, bool still_refreshing, const QList& list, const QString &preferred); }; diff --git a/app/dialog/preferences/tabs/preferencesqualitytab.cpp b/app/dialog/preferences/tabs/preferencesqualitytab.cpp index 39e37ca4c..32884c5f3 100644 --- a/app/dialog/preferences/tabs/preferencesqualitytab.cpp +++ b/app/dialog/preferences/tabs/preferencesqualitytab.cpp @@ -26,7 +26,6 @@ #include "audio/sampleformat.h" #include "render/colormanager.h" -#include "render/pixelformat.h" OLIVE_NAMESPACE_ENTER @@ -49,12 +48,12 @@ PreferencesQualityTab::PreferencesQualityTab() quality_stack_ = new QStackedWidget(); offline_group_ = new PreferencesQualityGroup(tr("Offline Quality")); - offline_group_->bit_depth_combobox()->setCurrentIndex(PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOffline)); + offline_group_->SetBitDepth(PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOffline)); offline_group_->ocio_method()->setCurrentIndex(ColorManager::GetOCIOMethodForMode(RenderMode::kOffline)); quality_stack_->addWidget(offline_group_); online_group_ = new PreferencesQualityGroup(tr("Online Quality")); - online_group_->bit_depth_combobox()->setCurrentIndex(PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOnline)); + online_group_->SetBitDepth(PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOnline)); online_group_->ocio_method()->setCurrentIndex(ColorManager::GetOCIOMethodForMode(RenderMode::kOnline)); quality_stack_->addWidget(online_group_); @@ -92,7 +91,8 @@ PreferencesQualityGroup::PreferencesQualityGroup(const QString &title, QWidget * PixelFormat::Format pix_fmt = static_cast(i); // We always render with an alpha channel internally - if (PixelFormat::FormatHasAlphaChannel(pix_fmt)) { + if (PixelFormat::FormatHasAlphaChannel(pix_fmt) + && PixelFormat::FormatIsFloat(pix_fmt)) { bit_depth_combobox_->addItem(PixelFormat::GetName(pix_fmt), i); } @@ -112,6 +112,16 @@ PreferencesQualityGroup::PreferencesQualityGroup(const QString &title, QWidget * quality_outer_layout->addStretch(); } +void PreferencesQualityGroup::SetBitDepth(PixelFormat::Format f) +{ + for (int i=0;icount();i++) { + if (bit_depth_combobox_->itemData(i) == f) { + bit_depth_combobox_->setCurrentIndex(i); + break; + } + } +} + QComboBox *PreferencesQualityGroup::bit_depth_combobox() { return bit_depth_combobox_; diff --git a/app/dialog/preferences/tabs/preferencesqualitytab.h b/app/dialog/preferences/tabs/preferencesqualitytab.h index 1219c485d..beae8ff62 100644 --- a/app/dialog/preferences/tabs/preferencesqualitytab.h +++ b/app/dialog/preferences/tabs/preferencesqualitytab.h @@ -26,6 +26,7 @@ #include #include +#include "render/pixelformat.h" #include "preferencestab.h" OLIVE_NAMESPACE_ENTER @@ -36,6 +37,8 @@ class PreferencesQualityGroup : public QGroupBox public: PreferencesQualityGroup(const QString& title, QWidget* parent = nullptr); + void SetBitDepth(PixelFormat::Format f); + QComboBox* bit_depth_combobox(); QComboBox* ocio_method(); diff --git a/app/main.cpp b/app/main.cpp index 859ed0b76..8713f4d96 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -50,8 +50,8 @@ int main(int argc, char *argv[]) { format.setProfile(QSurfaceFormat::CoreProfile); QSurfaceFormat::setDefaultFormat(format); - // Try to share OpenGL contexts - QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); // Create application instance QApplication a(argc, argv); diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index ccb3f0140..06e0237f6 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -234,6 +234,18 @@ void Block::SaveInternal(QXmlStreamWriter *writer) const } } +QList Block::GetInputsToHash() const +{ + QList inputs = Node::GetInputsToHash(); + + // Ignore these inputs + inputs.removeOne(media_in_input_); + inputs.removeOne(speed_input_); + inputs.removeOne(length_input_); + + return inputs; +} + void Block::LengthInputChanged() { emit LengthChanged(length()); diff --git a/app/node/block/block.h b/app/node/block/block.h index 26df9d13a..a36f91754 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -126,6 +126,8 @@ protected: virtual void SaveInternal(QXmlStreamWriter* writer) const override; + virtual QList GetInputsToHash() const override; + Block* previous_; Block* next_; diff --git a/app/node/block/transition/externaltransition.cpp b/app/node/block/transition/externaltransition.cpp index 3de007d4d..74bbf817d 100644 --- a/app/node/block/transition/externaltransition.cpp +++ b/app/node/block/transition/externaltransition.cpp @@ -40,6 +40,11 @@ QString ExternalTransition::Name() const return meta_.Name(); } +QString ExternalTransition::ShortName() const +{ + return meta_.ShortName(); +} + QString ExternalTransition::id() const { return meta_.id(); diff --git a/app/node/block/transition/externaltransition.h b/app/node/block/transition/externaltransition.h index de16936f9..91cbd0ca9 100644 --- a/app/node/block/transition/externaltransition.h +++ b/app/node/block/transition/externaltransition.h @@ -35,6 +35,7 @@ public: virtual Node* copy() const override; virtual QString Name() const override; + virtual QString ShortName() const override; virtual QString id() const override; virtual QString Category() const override; virtual QString Description() const override; diff --git a/app/node/block/transition/transition.cpp b/app/node/block/transition/transition.cpp index 91fe5adb6..cdb0b8489 100644 --- a/app/node/block/transition/transition.cpp +++ b/app/node/block/transition/transition.cpp @@ -129,6 +129,19 @@ double TransitionBlock::GetInProgress(const rational &time) const return clamp((GetInternalTransitionTime(time) - out_offset().toDouble()) / in_offset().toDouble(), 0.0, 1.0); } +void TransitionBlock::Hash(QCryptographicHash &hash, const rational &time) const +{ + Block::Hash(hash, time); + + double all_prog = GetTotalProgress(time); + double in_prog = GetInProgress(time); + double out_prog = GetOutProgress(time); + + hash.addData(reinterpret_cast(&all_prog), sizeof(double)); + hash.addData(reinterpret_cast(&in_prog), sizeof(double)); + hash.addData(reinterpret_cast(&out_prog), sizeof(double)); +} + double TransitionBlock::GetInternalTransitionTime(const rational &time) const { return time.toDouble() - in().toDouble(); diff --git a/app/node/block/transition/transition.h b/app/node/block/transition/transition.h index 74346c6a0..62f4280b5 100644 --- a/app/node/block/transition/transition.h +++ b/app/node/block/transition/transition.h @@ -47,6 +47,8 @@ public: double GetOutProgress(const rational& time) const; double GetInProgress(const rational& time) const; + virtual void Hash(QCryptographicHash& hash, const rational &time) const override; + private: double GetInternalTransitionTime(const rational& time) const; diff --git a/app/node/external.cpp b/app/node/external.cpp index 7bafc30e9..6d6a20ac9 100644 --- a/app/node/external.cpp +++ b/app/node/external.cpp @@ -42,6 +42,11 @@ QString ExternalNode::Name() const return meta_.Name(); } +QString ExternalNode::ShortName() const +{ + return meta_.ShortName(); +} + QString ExternalNode::id() const { return meta_.id(); diff --git a/app/node/external.h b/app/node/external.h index a20b11c15..b9a50a313 100644 --- a/app/node/external.h +++ b/app/node/external.h @@ -39,6 +39,7 @@ public: virtual Node* copy() const override; virtual QString Name() const override; + virtual QString ShortName() const override; virtual QString id() const override; virtual QString Category() const override; virtual QString Description() const override; diff --git a/app/node/generator/matrix/matrix.cpp b/app/node/generator/matrix/matrix.cpp index ad41d9554..604e36415 100644 --- a/app/node/generator/matrix/matrix.cpp +++ b/app/node/generator/matrix/matrix.cpp @@ -59,6 +59,11 @@ QString MatrixGenerator::Name() const return tr("Orthographic Matrix"); } +QString MatrixGenerator::ShortName() const +{ + return tr("Ortho"); +} + QString MatrixGenerator::id() const { return QStringLiteral("org.olivevideoeditor.Olive.transform"); diff --git a/app/node/generator/matrix/matrix.h b/app/node/generator/matrix/matrix.h index 817fcb58e..116ef1602 100644 --- a/app/node/generator/matrix/matrix.h +++ b/app/node/generator/matrix/matrix.h @@ -34,6 +34,7 @@ public: virtual Node* copy() const override; virtual QString Name() const override; + virtual QString ShortName() const override; virtual QString id() const override; virtual QString Category() const override; virtual QString Description() const override; diff --git a/app/node/input.cpp b/app/node/input.cpp index e025f5ed7..33ab85da4 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -737,6 +737,19 @@ void NodeInput::remove_keyframe(NodeKeyframePtr key) emit_time_range(time_affected); } +NodeKeyframePtr NodeInput::get_keyframe_shared_ptr_from_raw(NodeKeyframe* raw) const +{ + foreach (const KeyframeTrack& track, keyframe_tracks_) { + foreach (NodeKeyframePtr key, track) { + if (key.get() == raw) { + return key; + } + } + } + + return nullptr; +} + void NodeInput::KeyframeTimeChanged() { NodeKeyframe* key = static_cast(sender()); diff --git a/app/node/input.h b/app/node/input.h index c672907ee..2f271a4a1 100644 --- a/app/node/input.h +++ b/app/node/input.h @@ -172,6 +172,11 @@ public: */ void remove_keyframe(NodeKeyframePtr key); + /** + * @brief Hacky convenience function to turn a raw pointer into a shared pointer + */ + NodeKeyframePtr get_keyframe_shared_ptr_from_raw(NodeKeyframe *raw) const; + /** * @brief Return whether a keyframe exists at this time * @@ -279,7 +284,7 @@ public: QList GetImmediateDependencies() const; signals: - void ValueChanged(const TimeRange& range); + void ValueChanged(const OLIVE_NAMESPACE::TimeRange& range); void KeyframeEnableChanged(bool); diff --git a/app/node/input/time/timeinput.cpp b/app/node/input/time/timeinput.cpp index 60b901b8b..fa15de735 100644 --- a/app/node/input/time/timeinput.cpp +++ b/app/node/input/time/timeinput.cpp @@ -62,4 +62,12 @@ NodeValueTable TimeInput::Value(NodeValueDatabase &value) const return table; } +void TimeInput::Hash(QCryptographicHash &hash, const rational &time) const +{ + Node::Hash(hash, time); + + // Make sure time is hashed + hash.addData(NodeParam::ValueToBytes(NodeParam::kRational, QVariant::fromValue(time))); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/node/input/time/timeinput.h b/app/node/input/time/timeinput.h index 284faba4a..b912ed4b1 100644 --- a/app/node/input/time/timeinput.h +++ b/app/node/input/time/timeinput.h @@ -40,6 +40,8 @@ public: virtual NodeValueTable Value(NodeValueDatabase& value) const override; + virtual void Hash(QCryptographicHash& hash, const rational& time) const override; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/node/metareader.cpp b/app/node/metareader.cpp index b88b3d2e1..007d7ec7b 100644 --- a/app/node/metareader.cpp +++ b/app/node/metareader.cpp @@ -57,6 +57,15 @@ QString NodeMetaReader::Name() const return GetStringForCurrentLanguage(&names_); } +QString NodeMetaReader::ShortName() const +{ + if (short_names_.isEmpty()) { + return Name(); + } else { + return GetStringForCurrentLanguage(&short_names_); + } +} + const QString &NodeMetaReader::id() const { return id_; @@ -173,6 +182,9 @@ void NodeMetaReader::XMLReadEffect(QXmlStreamReader* reader) if (reader->name() == QStringLiteral("name")) { // Pick up name XMLReadLanguageString(reader, &names_); + } else if (reader->name() == QStringLiteral("shortnames")) { + // Pick up short name + XMLReadLanguageString(reader, &short_names_); } else if (reader->name() == QStringLiteral("category")) { // Pick up category XMLReadLanguageString(reader, &categories_); diff --git a/app/node/metareader.h b/app/node/metareader.h index dda052c83..13c965a87 100644 --- a/app/node/metareader.h +++ b/app/node/metareader.h @@ -35,6 +35,7 @@ public: NodeMetaReader(const QString& xml_meta_filename); QString Name() const; + QString ShortName() const; const QString& id() const; QString Category() const; QString Description() const; @@ -67,6 +68,7 @@ private: QString xml_filename_; LanguageMap names_; + LanguageMap short_names_; LanguageMap descriptions_; LanguageMap categories_; QMap param_names_; diff --git a/app/node/node.cpp b/app/node/node.cpp index e8749398b..76f82484c 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -25,6 +25,9 @@ #include #include "common/xmlutils.h" +#include "project/project.h" +#include "project/item/footage/footage.h" +#include "project/item/footage/imagestream.h" OLIVE_NAMESPACE_ENTER @@ -103,6 +106,13 @@ void Node::Save(QXmlStreamWriter *writer, const QString &custom_name) const writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); + writer->writeAttribute(QStringLiteral("pos"), + QStringLiteral("%1:%2").arg(QString::number(GetPosition().x()), + QString::number(GetPosition().y()))); + + writer->writeAttribute(QStringLiteral("label"), + GetLabel()); + foreach (NodeParam* param, parameters()) { param->Save(writer); } @@ -112,6 +122,11 @@ void Node::Save(QXmlStreamWriter *writer, const QString &custom_name) const writer->writeEndElement(); // node } +QString Node::ShortName() const +{ + return Name(); +} + QString Node::Category() const { // Return an empty category for any nodes that don't use one @@ -220,6 +235,11 @@ void Node::SaveInternal(QXmlStreamWriter *) const { } +QList Node::GetInputsToHash() const +{ + return GetInputsIncludingArrays(); +} + QString Node::ReadFileAsString(const QString &filename) { QFile f(filename); @@ -277,6 +297,83 @@ void Node::DrawGizmos(QPainter *, const QRect &) const { } +const QString &Node::GetLabel() const +{ + return label_; +} + +void Node::SetLabel(const QString &s) +{ + if (label_ != s) { + label_ = s; + + emit LabelChanged(label_); + } +} + +void Node::Hash(QCryptographicHash &hash, const rational& time) const +{ + // Add this Node's ID + hash.addData(id().toUtf8()); + + QList inputs = GetInputsToHash(); + + foreach (NodeInput* input, inputs) { + // For each input, try to hash its value + + // Get time adjustment + // For a single frame, we only care about one of the times + rational input_time = InputTimeAdjustment(input, TimeRange(time, time)).in(); + + if (input->IsConnected()) { + // Traverse down this edge + input->get_connected_node()->Hash(hash, input_time); + } else { + // Grab the value at this time + QVariant value = input->get_value_at_time(input_time); + hash.addData(NodeParam::ValueToBytes(input->data_type(), value)); + } + + // We have one exception for FOOTAGE types, since we resolve the footage into a frame in the renderer + if (input->data_type() == NodeParam::kFootage) { + StreamPtr stream = input->get_standard_value().value(); + + if (stream) { + // Add footage details to hash + + // Footage filename + hash.addData(stream->footage()->filename().toUtf8()); + + // Footage last modified date + hash.addData(stream->footage()->timestamp().toString().toUtf8()); + + // Footage stream + hash.addData(QString::number(stream->index()).toUtf8()); + + if (stream->type() == Stream::kImage || stream->type() == Stream::kVideo) { + ImageStreamPtr image_stream = std::static_pointer_cast(stream); + + // Current color config and space + hash.addData(image_stream->footage()->project()->color_manager()->GetConfigFilename().toUtf8()); + hash.addData(image_stream->colorspace().toUtf8()); + + // Alpha associated setting + hash.addData(QString::number(image_stream->premultiplied_alpha()).toUtf8()); + } + + // Footage timestamp + if (stream->type() == Stream::kVideo) { + hash.addData(QStringLiteral("%1/%2").arg(QString::number(input_time.numerator()), + QString::number(input_time.denominator())).toUtf8()); + + hash.addData(QString::number(static_cast(stream.get())->start_time()).toUtf8()); + + } + } + } + } +} + void Node::CopyInputs(Node *source, Node *destination, bool include_connections) { Q_ASSERT(source->id() == destination->id()); @@ -295,6 +392,9 @@ void Node::CopyInputs(Node *source, Node *destination, bool include_connections) NodeInput::CopyValues(src, dst, include_connections); } } + + destination->SetPosition(source->GetPosition()); + destination->SetLabel(source->GetLabel()); } bool Node::CanBeDeleted() const @@ -559,7 +659,7 @@ NodeValue Node::InputValueFromTable(NodeInput *input, NodeValueDatabase &db, boo } } -const QPointF &Node::GetPosition() +const QPointF &Node::GetPosition() const { return position_; } @@ -567,6 +667,8 @@ const QPointF &Node::GetPosition() void Node::SetPosition(const QPointF &pos) { position_ = pos; + + emit PositionChanged(position_); } void Node::AddInput(NodeInput *input) diff --git a/app/node/node.h b/app/node/node.h index bfe36b26b..b623ca1d5 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -92,6 +92,13 @@ public: */ virtual QString Name() const = 0; + /** + * @brief Returns a shortened name of this node if applicable + * + * Defaults to returning Name() but can be overridden. + */ + virtual QString ShortName() const; + /** * @brief Return the unique identifier of the node * @@ -343,7 +350,7 @@ public: virtual NodeValue InputValueFromTable(NodeInput* input, NodeValueDatabase &db, bool take) const; - const QPointF& GetPosition(); + const QPointF& GetPosition() const; void SetPosition(const QPointF& pos); @@ -357,6 +364,11 @@ public: virtual void DrawGizmos(QPainter* p, const QRect &viewport) const; + const QString& GetLabel() const; + void SetLabel(const QString& s); + + virtual void Hash(QCryptographicHash& hash, const rational &time) const; + protected: void AddInput(NodeInput* input); @@ -368,6 +380,8 @@ protected: virtual void SaveInternal(QXmlStreamWriter* writer) const; + virtual QList GetInputsToHash() const; + public slots: signals: @@ -389,6 +403,16 @@ signals: */ void EdgeRemoved(NodeEdgePtr edge); + /** + * @brief Signal emitted whenever the position is set through SetPosition() + */ + void PositionChanged(const QPointF& pos); + + /** + * @brief Signal emitted when SetLabel() is called + */ + void LabelChanged(const QString& s); + private: /** * @brief Add a parameter to this node @@ -424,8 +448,13 @@ private: */ QPointF position_; + /** + * @brief Custom user label for node + */ + QString label_; + private slots: - void InputChanged(const TimeRange &range); + void InputChanged(const OLIVE_NAMESPACE::TimeRange &range); void InputConnectionChanged(NodeEdgePtr edge); diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 24497f684..da545cd6e 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -418,6 +418,16 @@ NodeInputArray *TrackOutput::block_input() const return block_input_; } +void TrackOutput::Hash(QCryptographicHash &hash, const rational &time) const +{ + // Resolve block list + Block* b = BlockAtTime(time); + + if (b) { + return b->Hash(hash, time); + } +} + void TrackOutput::SetTrackName(const QString &name) { track_name_ = name; @@ -556,9 +566,7 @@ void TrackOutput::BlockDisconnected(NodeEdgePtr edge) block_cache_.removeAt(index_of_block); // If there were blocks following this one, update their ins/outs - if (index_of_block < block_cache_.size()) { - UpdateInOutFrom(index_of_block); - } + UpdateInOutFrom(index_of_block); // Join the previous and next blocks together if (connected_block->previous()) { diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 89f58903a..0922d54fc 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -152,6 +152,8 @@ public: NodeInputArray* block_input() const; + virtual void Hash(QCryptographicHash& hash, const rational &time) const override; + public slots: void SetTrackName(const QString& name); diff --git a/app/panel/curve/curve.cpp b/app/panel/curve/curve.cpp index 233b637f5..2c9fc0e4b 100644 --- a/app/panel/curve/curve.cpp +++ b/app/panel/curve/curve.cpp @@ -37,6 +37,11 @@ NodeInput *CurvePanel::GetInput() const return static_cast(GetTimeBasedWidget())->GetInput(); } +void CurvePanel::DeleteSelected() +{ + static_cast(GetTimeBasedWidget())->DeleteSelected(); +} + void CurvePanel::SetInput(NodeInput *input) { static_cast(GetTimeBasedWidget())->SetInput(input); diff --git a/app/panel/curve/curve.h b/app/panel/curve/curve.h index 761b00a0f..4b25b2b22 100644 --- a/app/panel/curve/curve.h +++ b/app/panel/curve/curve.h @@ -34,6 +34,8 @@ public: NodeInput* GetInput() const; + virtual void DeleteSelected() override; + public slots: void SetInput(NodeInput* input); diff --git a/app/panel/node/node.cpp b/app/panel/node/node.cpp index cbeef8d2a..892b0c32f 100644 --- a/app/panel/node/node.cpp +++ b/app/panel/node/node.cpp @@ -73,6 +73,11 @@ void NodePanel::Paste() node_view_->Paste(); } +void NodePanel::Duplicate() +{ + node_view_->Duplicate(); +} + void NodePanel::Select(const QList &nodes) { node_view_->Select(nodes); diff --git a/app/panel/node/node.h b/app/panel/node/node.h index c2f07a482..9dc9533b8 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -47,6 +47,8 @@ public: virtual void Paste() override; + virtual void Duplicate() override; + public slots: void Select(const QList& nodes); void SelectWithDependencies(const QList& nodes); diff --git a/app/panel/panelmanager.cpp b/app/panel/panelmanager.cpp index f85e6824f..53903f81e 100644 --- a/app/panel/panelmanager.cpp +++ b/app/panel/panelmanager.cpp @@ -35,10 +35,10 @@ PanelManager::PanelManager(QObject *parent) : void PanelManager::DeleteAllPanels() { - foreach (PanelWidget* panel, focus_history_) { - delete panel; - } + // Prevent any confusion regarding focus history by clearing it first + QList copy = focus_history_; focus_history_.clear(); + qDeleteAll(copy); } const QList &PanelManager::panels() diff --git a/app/panel/panelmanager.h b/app/panel/panelmanager.h index 7acba73e3..caa7bae07 100644 --- a/app/panel/panelmanager.h +++ b/app/panel/panelmanager.h @@ -175,14 +175,26 @@ T *PanelManager::CreatePanel(QWidget *parent) { T* panel = new T(parent); - panel->SetMovementLocked(locked_); - - // Connect destroy signal so we can remove it from focus history - connect(panel, &PanelWidget::destroyed, this, &PanelManager::PanelDestroyed); - // Add panel to the bottom of the focus history focus_history_.append(panel); + panel->SetMovementLocked(locked_); + + // Sane default for panel size + panel->resize(parent->size() / 3); + + // We're about to center the panel relative to the parent (usually the main window), but for some + // reason this requires the panel to be shown first. + panel->show(); + + // Center the panel relative to the parent + QPoint parent_center = panel->mapFromGlobal(parent->mapToGlobal(parent->rect().center())); + QPoint panel_center = panel->rect().center(); + panel->move(parent_center - panel_center); + + // Connect destroy signal so we can remove it from focus history + connect(panel, &PanelWidget::destroyed, this, &PanelManager::PanelDestroyed, Qt::DirectConnection); + return panel; } diff --git a/app/panel/param/param.cpp b/app/panel/param/param.cpp index 0112f80fb..649998cdf 100644 --- a/app/panel/param/param.cpp +++ b/app/panel/param/param.cpp @@ -60,6 +60,11 @@ void ParamPanel::SetTimestamp(const int64_t ×tamp) } } +void ParamPanel::DeleteSelected() +{ + static_cast(GetTimeBasedWidget())->DeleteSelected(); +} + void ParamPanel::Retranslate() { SetTitle(tr("Parameter Editor")); @@ -95,9 +100,11 @@ void ParamPanel::CreateCurvePanel(NodeInput *input) panel->SetInput(input); panel->SetTimebase(view->timebase()); panel->SetTimestamp(view->GetTimestamp()); + panel->SetTimeTarget(view->GetTimeTarget()); connect(view, &NodeParamView::TimebaseChanged, panel, &CurvePanel::SetTimebase); connect(view, &NodeParamView::TimeChanged, panel, &CurvePanel::SetTimestamp); + connect(view, &NodeParamView::TimeTargetChanged, panel, &CurvePanel::SetTimeTarget); connect(panel, &CurvePanel::TimeChanged, view, &NodeParamView::SetTimestamp); connect(panel, &CurvePanel::TimeChanged, view, &NodeParamView::TimeChanged); connect(panel, &CurvePanel::CloseRequested, this, &ParamPanel::ClosingCurvePanel); diff --git a/app/panel/param/param.h b/app/panel/param/param.h index fcb485bb4..0227e45ba 100644 --- a/app/panel/param/param.h +++ b/app/panel/param/param.h @@ -38,6 +38,8 @@ public slots: virtual void SetTimestamp(const int64_t& timestamp) override; + virtual void DeleteSelected() override; + signals: void TimeTargetChanged(Node* node); diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index b8f5d42e0..51fd9388e 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -49,12 +49,16 @@ ProjectPanel::ProjectPanel(QWidget *parent) : layout->addWidget(toolbar); // Make toolbar connections - connect(toolbar, SIGNAL(NewClicked()), this, SLOT(ShowNewMenu())); + connect(toolbar, &ProjectToolbar::NewClicked, this, &ProjectPanel::ShowNewMenu); + connect(toolbar, &ProjectToolbar::OpenClicked, Core::instance(), &Core::OpenProject); + connect(toolbar, &ProjectToolbar::SaveClicked, Core::instance(), &Core::SaveActiveProject); + connect(toolbar, &ProjectToolbar::UndoClicked, Core::instance()->undo_stack(), &QUndoStack::undo); + connect(toolbar, &ProjectToolbar::RedoClicked, Core::instance()->undo_stack(), &QUndoStack::redo); // Set up main explorer object explorer_ = new ProjectExplorer(this); layout->addWidget(explorer_); - connect(explorer_, SIGNAL(DoubleClickedItem(Item*)), this, SLOT(ItemDoubleClickSlot(Item*))); + connect(explorer_, &ProjectExplorer::DoubleClickedItem, this, &ProjectPanel::ItemDoubleClickSlot); // Set toolbar's view to the explorer's view toolbar->SetView(explorer_->view_type()); diff --git a/app/panel/scope/scope.cpp b/app/panel/scope/scope.cpp index 222fcec64..5daa7e9e0 100644 --- a/app/panel/scope/scope.cpp +++ b/app/panel/scope/scope.cpp @@ -84,19 +84,10 @@ QString ScopePanel::TypeToName(ScopePanel::Type t) return QString(); } -void ScopePanel::SetDisplayReferredTexture(OpenGLTexture *texture) -{ - Q_UNUSED(texture) -} - void ScopePanel::SetReferenceBuffer(Frame *frame) { histogram_->SetBuffer(frame); -} - -void ScopePanel::SetReferenceTexture(OpenGLTexture *texture) -{ - waveform_view_->SetTexture(texture); + waveform_view_->SetBuffer(frame); } void ScopePanel::SetColorManager(ColorManager *manager) diff --git a/app/panel/scope/scope.h b/app/panel/scope/scope.h index f44e37aa6..3e771af69 100644 --- a/app/panel/scope/scope.h +++ b/app/panel/scope/scope.h @@ -50,12 +50,8 @@ public: static QString TypeToName(Type t); public slots: - void SetDisplayReferredTexture(OpenGLTexture* texture); - void SetReferenceBuffer(Frame* frame); - void SetReferenceTexture(OpenGLTexture* texture); - void SetColorManager(ColorManager* manager); protected: diff --git a/app/panel/viewer/viewerbase.cpp b/app/panel/viewer/viewerbase.cpp index a1899e5f3..9f9e4c84e 100644 --- a/app/panel/viewer/viewerbase.cpp +++ b/app/panel/viewer/viewerbase.cpp @@ -25,8 +25,7 @@ OLIVE_NAMESPACE_ENTER ViewerPanelBase::ViewerPanelBase(const QString& object_name, QWidget *parent) : - TimeBasedPanel(object_name, parent), - scope_panel_count_(0) + TimeBasedPanel(object_name, parent) { } @@ -103,33 +102,13 @@ void ViewerPanelBase::CreateScopePanel(ScopePanel::Type type) p->SetType(type); - // If the scope closes, reduce the count (we do this because if no scopes are open, we can optimize the viewer slightly) - connect(p, &ScopePanel::CloseRequested, this, &ViewerPanelBase::ScopePanelClosed); - // Connect viewer widget texture drawing to scope panel - connect(vw, &ViewerWidget::DrewManagedTexture, p, &ScopePanel::SetDisplayReferredTexture); connect(vw, &ViewerWidget::LoadedBuffer, p, &ScopePanel::SetReferenceBuffer); - connect(vw, &ViewerWidget::LoadedTexture, p, &ScopePanel::SetReferenceTexture); connect(vw, &ViewerWidget::ColorManagerChanged, p, &ScopePanel::SetColorManager); p->SetColorManager(vw->color_manager()); - if (!scope_panel_count_) { - vw->SetEmitDrewManagedTextureEnabled(true); - } - - scope_panel_count_++; - vw->ForceUpdate(); } -void ViewerPanelBase::ScopePanelClosed() -{ - scope_panel_count_--; - - if (!scope_panel_count_) { - static_cast(GetTimeBasedWidget())->SetEmitDrewManagedTextureEnabled(false); - } -} - OLIVE_NAMESPACE_EXIT diff --git a/app/panel/viewer/viewerbase.h b/app/panel/viewer/viewerbase.h index a74f13244..ed4c5651a 100644 --- a/app/panel/viewer/viewerbase.h +++ b/app/panel/viewer/viewerbase.h @@ -62,12 +62,6 @@ public slots: protected: void CreateScopePanel(ScopePanel::Type type); -private: - int scope_panel_count_; - -private slots: - void ScopePanelClosed(); - }; OLIVE_NAMESPACE_EXIT diff --git a/app/project/projectloadmanager.h b/app/project/projectloadmanager.h index 1e656ff26..6343293c6 100644 --- a/app/project/projectloadmanager.h +++ b/app/project/projectloadmanager.h @@ -36,7 +36,7 @@ protected: virtual void Action() override; signals: - void ProjectLoaded(ProjectPtr project); + void ProjectLoaded(OLIVE_NAMESPACE::ProjectPtr project); private: QString filename_; diff --git a/app/project/projectsavemanager.h b/app/project/projectsavemanager.h index 88be5f840..fb5d1fe79 100644 --- a/app/project/projectsavemanager.h +++ b/app/project/projectsavemanager.h @@ -33,7 +33,7 @@ public: ProjectSaveManager(ProjectPtr project); signals: - void ProjectSaveSucceeded(ProjectPtr p); + void ProjectSaveSucceeded(OLIVE_NAMESPACE::ProjectPtr p); protected: virtual void Action() override; diff --git a/app/render/backend/audio/audiobackend.cpp b/app/render/backend/audio/audiobackend.cpp index 6b6f81ab2..f7450542a 100644 --- a/app/render/backend/audio/audiobackend.cpp +++ b/app/render/backend/audio/audiobackend.cpp @@ -65,43 +65,47 @@ void AudioBackend::ThreadCompletedCache(NodeDependency dep, NodeValueTable data, if (job_time == render_job_info_.value(dep.range())) { render_job_info_.remove(dep.range()); - QByteArray cached_samples = data.Get(NodeParam::kSamples).value()->toPackedData(); + SampleBufferPtr cached_sample_ptr = data.Get(NodeParam::kSamples).value(); - int offset = params().time_to_bytes(dep.in()); - int length = params().time_to_bytes(dep.range().length()); - int out_point = qMin(offset + length, params().time_to_bytes(GetSequenceLength())); + if (cached_sample_ptr) { + QByteArray cached_samples = cached_sample_ptr->toPackedData(); - if (offset < out_point) { - if (offset + length > out_point) { - length = out_point - offset; - } + int offset = params().time_to_bytes(dep.in()); + int length = params().time_to_bytes(dep.range().length()); + int out_point = qMin(offset + length, params().time_to_bytes(GetSequenceLength())); - QFile f(CachePathName()); - if (f.open(QFile::ReadWrite)) { - - if (f.size() < out_point && !f.resize(out_point)) { - qCritical() << "Failed to resize file" << CachePathName(); + if (offset < out_point) { + if (offset + length > out_point) { + length = out_point - offset; } - if (!f.seek(offset)) { - qCritical() << "Failed to seek file" << CachePathName(); + QFile f(CachePathName()); + if (f.open(QFile::ReadWrite)) { + + if (f.size() < out_point && !f.resize(out_point)) { + qCritical() << "Failed to resize file" << CachePathName(); + } + + if (!f.seek(offset)) { + qCritical() << "Failed to seek file" << CachePathName(); + } + + // Replace data with this data + int copy_length = qMin(length, cached_samples.size()); + + f.write(cached_samples.data(), copy_length); + + if (copy_length < length) { + + // Fill in remainder with silence + QByteArray empty_space(length - copy_length, 0); + f.write(empty_space); + } + + f.close(); + } else { + qWarning() << "Failed to write to cached PCM file"; } - - // Replace data with this data - int copy_length = qMin(length, cached_samples.size()); - - f.write(cached_samples.data(), copy_length); - - if (copy_length < length) { - - // Fill in remainder with silence - QByteArray empty_space(length - copy_length, 0); - f.write(empty_space); - } - - f.close(); - } else { - qWarning() << "Failed to write to cached PCM file"; } } } diff --git a/app/render/backend/exporter.cpp b/app/render/backend/exporter.cpp index aa8af3165..5ec95611c 100644 --- a/app/render/backend/exporter.cpp +++ b/app/render/backend/exporter.cpp @@ -20,6 +20,8 @@ #include "exporter.h" +#include + #include "render/backend/audio/audiobackend.h" #include "render/backend/opengl/openglbackend.h" #include "render/colormanager.h" @@ -177,17 +179,6 @@ void Exporter::EncodeFrame() while (cached_frames_.contains(waiting_for_frame_)) { FramePtr frame = cached_frames_.take(waiting_for_frame_); - // OCIO conversion requires a frame in 32F format - if (frame->format() != PixelFormat::PIX_FMT_RGBA32F) { - frame = PixelFormat::ConvertPixelFormat(frame, PixelFormat::PIX_FMT_RGBA32F); - } - - // Color conversion must be done with unassociated alpha, and the pipeline is always associated - ColorManager::DisassociateAlpha(frame); - - // Convert color space - color_processor_->ConvertFrame(frame); - // Encode (may require re-associating alpha?) QMetaObject::invokeMethod(encoder_, "WriteFrame", @@ -233,29 +224,39 @@ QMatrix4x4 Exporter::GenerateMatrix(ExportParams::VideoScalingMethod method, int return preview_matrix; } -void Exporter::FrameRendered(const rational &time, FramePtr value) +FramePtr FrameColorConvert(ColorProcessorPtr processor, FramePtr frame) { - debug_timer_.stop(); + qDebug() << "Converting" << frame->timestamp(); - const QMap& time_hash_map = video_backend_->frame_cache()->time_hash_map(); - - QByteArray this_hash = time_hash_map.value(time); - - qDebug() << "Received" << this_hash.toHex(); - - QList matching_times = time_hash_map.keys(this_hash); - - foreach (const rational& t, matching_times) { - qDebug() << " Matches" << t.toDouble(); - - cached_frames_.insert(t, value); + // OCIO conversion requires a frame in 32F format + if (frame->format() != PixelFormat::PIX_FMT_RGBA32F) { + frame = PixelFormat::ConvertPixelFormat(frame, PixelFormat::PIX_FMT_RGBA32F); } - qDebug() << " Waiting for" << waiting_for_frame_.toDouble(); + // Color conversion must be done with unassociated alpha, and the pipeline is always associated + ColorManager::DisassociateAlpha(frame); - debug_timer_.start(); + // Convert color space + processor->ConvertFrame(frame); - EncodeFrame(); + // Re-associate alpha + ColorManager::ReassociateAlpha(frame); + + return frame; +} + +void Exporter::FrameRendered(FramePtr frame) +{ + // Start color space conversion in another thread + QFutureWatcher* watcher = new QFutureWatcher(); + + connect(watcher, &QFutureWatcher::finished, this, &Exporter::FrameColorFinished); + + QFuture future = QtConcurrent::run(FrameColorConvert, + color_processor_, + frame); + + watcher->setFuture(future); } void Exporter::AudioRendered() @@ -353,4 +354,37 @@ void Exporter::DebugTimerMessage() qDebug() << "Still waiting for" << waiting_for_frame_.toDouble(); } +void Exporter::FrameColorFinished() +{ + if (!video_backend_ && !audio_backend_) { + return; + } + + QFutureWatcher* watcher = static_cast< QFutureWatcher* >(sender()); + FramePtr frame = watcher->result(); + watcher->deleteLater(); + + debug_timer_.stop(); + + const QMap& time_hash_map = video_backend_->frame_cache()->time_hash_map(); + + QByteArray this_hash = time_hash_map.value(frame->timestamp()); + + qDebug() << "Received" << this_hash.toHex(); + + QList matching_times = time_hash_map.keys(this_hash); + + foreach (const rational& t, matching_times) { + qDebug() << " Matches" << t.toDouble(); + + cached_frames_.insert(t, frame); + } + + qDebug() << " Waiting for" << waiting_for_frame_.toDouble(); + + debug_timer_.start(); + + EncodeFrame(); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/exporter.h b/app/render/backend/exporter.h index 3aaec6986..6c07a0dcd 100644 --- a/app/render/backend/exporter.h +++ b/app/render/backend/exporter.h @@ -101,7 +101,7 @@ private: QTimer debug_timer_; private slots: - void FrameRendered(const rational &time, FramePtr value); + void FrameRendered(FramePtr frame); void AudioRendered(); @@ -117,6 +117,8 @@ private slots: void DebugTimerMessage(); + void FrameColorFinished(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index d1f4f7ddd..194ac6dc4 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -132,7 +132,7 @@ void OpenGLProxy::FrameToValue(FramePtr frame, StreamPtr stream, NodeValueTable* VideoRenderingParams footage_params(frame->width(), frame->height(), frame->format()); - footage_tex_ref = texture_cache_.Get(ctx_, footage_params, frame->data(), frame->linesize_pixels()); + footage_tex_ref = texture_cache_.Get(ctx_, footage_params, frame); if (ocio_method == ColorManager::kOCIOFast) { if (!color_processor->IsEnabled()) { diff --git a/app/render/backend/opengl/opengltexture.cpp b/app/render/backend/opengl/opengltexture.cpp index c0c1621ea..d5013c10f 100644 --- a/app/render/backend/opengl/opengltexture.cpp +++ b/app/render/backend/opengl/opengltexture.cpp @@ -74,6 +74,11 @@ void OpenGLTexture::Create(QOpenGLContext *ctx, int width, int height, const Pix } void OpenGLTexture::Create(QOpenGLContext *ctx, FramePtr frame) +{ + Create(ctx, frame.get()); +} + +void OpenGLTexture::Create(QOpenGLContext *ctx, Frame *frame) { Create(ctx, frame->width(), frame->height(), frame->format(), frame->data(), frame->linesize_pixels()); } @@ -120,6 +125,16 @@ const GLuint &OpenGLTexture::texture() const return texture_; } +void OpenGLTexture::Upload(FramePtr frame) +{ + Upload(frame.get()); +} + +void OpenGLTexture::Upload(Frame *frame) +{ + Upload(frame->data(), frame->linesize_pixels()); +} + void OpenGLTexture::Upload(const void *data, int linesize) { if (!IsCreated()) { diff --git a/app/render/backend/opengl/opengltexture.h b/app/render/backend/opengl/opengltexture.h index 941e2aae8..70fa653f7 100644 --- a/app/render/backend/opengl/opengltexture.h +++ b/app/render/backend/opengl/opengltexture.h @@ -44,6 +44,7 @@ public: void Create(QOpenGLContext* ctx, int width, int height, const PixelFormat::Format &format, const void *data, int linesize); void Create(QOpenGLContext* ctx, int width, int height, const PixelFormat::Format &format); void Create(QOpenGLContext* ctx, FramePtr frame); + void Create(QOpenGLContext* ctx, Frame* frame); bool IsCreated() const; @@ -59,6 +60,8 @@ public: const GLuint& texture() const; + void Upload(FramePtr frame); + void Upload(Frame* frame); void Upload(const void *data, int linesize); public slots: diff --git a/app/render/backend/opengl/opengltexturecache.cpp b/app/render/backend/opengl/opengltexturecache.cpp index 95e0638ee..a54994467 100644 --- a/app/render/backend/opengl/opengltexturecache.cpp +++ b/app/render/backend/opengl/opengltexturecache.cpp @@ -29,6 +29,16 @@ OpenGLTextureCache::~OpenGLTextureCache() } } +OpenGLTextureCache::ReferencePtr OpenGLTextureCache::Get(QOpenGLContext *ctx, const VideoRenderingParams ¶ms, FramePtr frame) +{ + return Get(ctx, params, frame.get()); +} + +OpenGLTextureCache::ReferencePtr OpenGLTextureCache::Get(QOpenGLContext *ctx, const VideoRenderingParams ¶ms, Frame *frame) +{ + return Get(ctx, params, frame->data(), frame->linesize_pixels()); +} + OpenGLTextureCache::ReferencePtr OpenGLTextureCache::Get(QOpenGLContext* ctx, const VideoRenderingParams ¶ms, const void *data, int linesize) { OpenGLTexturePtr texture = nullptr; diff --git a/app/render/backend/opengl/opengltexturecache.h b/app/render/backend/opengl/opengltexturecache.h index 7be07bfbb..2baa87a67 100644 --- a/app/render/backend/opengl/opengltexturecache.h +++ b/app/render/backend/opengl/opengltexturecache.h @@ -57,6 +57,8 @@ public: DISABLE_COPY_MOVE(OpenGLTextureCache) + ReferencePtr Get(QOpenGLContext *ctx, const VideoRenderingParams& params, FramePtr frame); + ReferencePtr Get(QOpenGLContext *ctx, const VideoRenderingParams& params, Frame* frame); ReferencePtr Get(QOpenGLContext *ctx, const VideoRenderingParams& params, const void *data, int linesize); ReferencePtr Get(QOpenGLContext *ctx, const VideoRenderingParams& params); diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index 947cb2f1e..8ba8409f2 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -78,7 +78,7 @@ void RenderWorker::RunNodeAccelerated(const Node *node, const TimeRange &range, StreamPtr RenderWorker::ResolveStreamFromInput(NodeInput *input) { - return input->get_value_at_time(0).value(); + return input->get_standard_value().value(); } DecoderPtr RenderWorker::ResolveDecoderFromInput(StreamPtr stream) diff --git a/app/render/backend/videorenderbackend.h b/app/render/backend/videorenderbackend.h index 7391013c7..852d075ea 100644 --- a/app/render/backend/videorenderbackend.h +++ b/app/render/backend/videorenderbackend.h @@ -110,7 +110,7 @@ signals: void RangeInvalidated(const TimeRange& range); - void GeneratedFrame(const rational &time, FramePtr frame); + void GeneratedFrame(FramePtr frame); private: bool TimeIsQueued(const TimeRange &time) const; diff --git a/app/render/backend/videorenderworker.cpp b/app/render/backend/videorenderworker.cpp index 5dabdc5cb..f64a0bdbd 100644 --- a/app/render/backend/videorenderworker.cpp +++ b/app/render/backend/videorenderworker.cpp @@ -75,7 +75,7 @@ NodeValueTable VideoRenderWorker::RenderInternal(const NodeDependency& path, con hasher.addData(reinterpret_cast(&vfmt), sizeof(PixelFormat::Format)); hasher.addData(reinterpret_cast(&vmode), sizeof(RenderMode::Mode)); - HashNodeRecursively(&hasher, path.node(), path.in()); + path.node()->Hash(hasher, path.in()); hash = hasher.result(); } @@ -121,117 +121,6 @@ NodeValueTable VideoRenderWorker::RenderInternal(const NodeDependency& path, con return value; } -void VideoRenderWorker::HashNodeRecursively(QCryptographicHash *hash, const Node* n, const rational& time) -{ - // Resolve BlockList - if (n->IsTrack()) { - n = static_cast(n)->BlockAtTime(time); - - if (!n) { - return; - } - } - - // Add this Node's ID - hash->addData(n->id().toUtf8()); - - if (n->IsBlock() && static_cast(n)->type() == Block::kTransition) { - const TransitionBlock* transition = static_cast(n); - - double all_prog = transition->GetTotalProgress(time); - double in_prog = transition->GetInProgress(time); - double out_prog = transition->GetOutProgress(time); - - hash->addData(reinterpret_cast(&all_prog), sizeof(double)); - hash->addData(reinterpret_cast(&in_prog), sizeof(double)); - hash->addData(reinterpret_cast(&out_prog), sizeof(double)); - } - - foreach (NodeParam* param, n->parameters()) { - // For each input, try to hash its value - if (param->type() == NodeParam::kInput) { - NodeInput* input = static_cast(param); - - if (n->IsBlock()) { - const Block* b = static_cast(n); - - // Ignore some Block attributes when hashing - if (input == b->media_in_input() - || input == b->speed_input() - || input == b->length_input()) { - continue; - } - } - - // Get time adjustment - // For a single frame, we only care about one of the times - rational input_time = n->InputTimeAdjustment(input, TimeRange(time, time)).in(); - - if (input->IsConnected()) { - // Traverse down this edge - HashNodeRecursively(hash, input->get_connected_node(), input_time); - } else { - // Grab the value at this time - QVariant value = input->get_value_at_time(input_time); - hash->addData(NodeParam::ValueToBytes(input->data_type(), value)); - } - - // We have one exception for FOOTAGE types, since we resolve the footage into a frame in the renderer - if (input->data_type() == NodeParam::kFootage) { - StreamPtr stream = ResolveStreamFromInput(input); - - if (stream) { - DecoderPtr decoder = ResolveDecoderFromInput(stream); - - if (decoder) { - - // Add footage details to hash - - // Footage filename - hash->addData(stream->footage()->filename().toUtf8()); - - // Footage last modified date - hash->addData(stream->footage()->timestamp().toString().toUtf8()); - - // Footage stream - hash->addData(QString::number(stream->index()).toUtf8()); - - if (stream->type() == Stream::kImage || stream->type() == Stream::kVideo) { - ImageStreamPtr image_stream = std::static_pointer_cast(stream); - - // Current color config and space - hash->addData(image_stream->footage()->project()->color_manager()->GetConfigFilename().toUtf8()); - hash->addData(image_stream->colorspace().toUtf8()); - - // Alpha associated setting - hash->addData(QString::number(image_stream->premultiplied_alpha()).toUtf8()); - } - - // Footage timestamp - if (stream->type() == Stream::kVideo) { - hash->addData(QStringLiteral("%1/%2").arg(QString::number(input_time.numerator()), - QString::number(input_time.denominator())).toUtf8()); - - hash->addData(QString::number(static_cast(stream.get())->start_time()).toUtf8()); - /*Decoder::RetrieveState state = decoder->GetRetrieveState(input_time); - - if (state == Decoder::kReady) { - VideoStreamPtr video_stream = std::static_pointer_cast(stream); - - int64_t timestamp_here = video_stream->get_closest_timestamp_in_frame_index(input_time); - - hash->addData(QString::number(timestamp_here).toUtf8()); - } else { - ReportUnavailableFootage(stream, state, input_time); - }*/ - } - } - } - } - } - } -} - void VideoRenderWorker::SetParameters(const VideoRenderingParams &video_params) { video_params_ = video_params; @@ -380,7 +269,9 @@ void VideoRenderWorker::Download(const rational& time, QVariant texture, QString TextureToBuffer(texture, frame->width(), frame->height(), frame_gen_mat_, frame->data(), frame->linesize_pixels()); } - emit GeneratedFrame(time, frame); + frame->set_timestamp(time); + + emit GeneratedFrame(frame); } } diff --git a/app/render/backend/videorenderworker.h b/app/render/backend/videorenderworker.h index 6d43dbf6f..b1813b9b8 100644 --- a/app/render/backend/videorenderworker.h +++ b/app/render/backend/videorenderworker.h @@ -79,7 +79,7 @@ signals: void HashAlreadyExists(NodeDependency path, qint64 job_time, QByteArray hash); - void GeneratedFrame(const rational &time, FramePtr frame); + void GeneratedFrame(FramePtr frame); void Aborted(); @@ -105,8 +105,6 @@ protected: ColorProcessorCache* color_cache(); private: - void HashNodeRecursively(QCryptographicHash* hash, const Node *n, const rational &time); - void Download(const rational &time, QVariant texture, QString filename); void ResizeDownloadBuffer(); diff --git a/app/render/colormanager.cpp b/app/render/colormanager.cpp index f28a8ac29..3a907b3ae 100644 --- a/app/render/colormanager.cpp +++ b/app/render/colormanager.cpp @@ -61,6 +61,16 @@ OCIO::ConstConfigRcPtr ColorManager::GetDefaultConfig() void ColorManager::SetUpDefaultConfig() { + if (!qgetenv("OCIO").isEmpty()) { + try { + default_config_ = OCIO::Config::CreateFromEnv(); + + return; + } catch (OCIO::Exception& e) { + qWarning() << "Failed to load config from OCIO environment variable config:" << e.what(); + } + } + // Kind of hacky, but it'll work QString dir = QDir(FileFunctions::GetTempFilePath()).filePath(QStringLiteral("ocioconf")); diff --git a/app/render/pixelformat.cpp b/app/render/pixelformat.cpp index aa05beac8..2ddd251c1 100644 --- a/app/render/pixelformat.cpp +++ b/app/render/pixelformat.cpp @@ -25,6 +25,7 @@ #include #include +#include "codec/oiio/oiiodecoder.h" #include "common/define.h" #include "core.h" @@ -213,19 +214,39 @@ FramePtr PixelFormat::ConvertPixelFormat(FramePtr frame, const PixelFormat::Form return frame; } + // Create a destination frame with the same parameters FramePtr converted = Frame::Create(); - - // Copy parameters converted->set_video_params(VideoRenderingParams(frame->video_params().width(), frame->video_params().height(), dest_format)); converted->set_timestamp(frame->timestamp()); converted->allocate(); - OIIO::ImageBuf src(OIIO::ImageSpec(frame->width(), frame->height(), ChannelCount(frame->format()), GetOIIOTypeDesc(frame->format())), frame->data()); - OIIO::ImageBuf dst(OIIO::ImageSpec(converted->width(), converted->height(), ChannelCount(converted->format()), GetOIIOTypeDesc(converted->format())), converted->data()); + // Do the conversion through OIIO - create a buffer for the source image + OIIO::ImageBuf src(OIIO::ImageSpec(frame->width(), + frame->height(), + ChannelCount(frame->format()), + GetOIIOTypeDesc(frame->format()))); + + // Set the pixels (this is necessary as opposed to an OIIO buffer wrapper since Frame has + // linesizes) + src.set_pixels(OIIO::ROI(), + GetOIIOTypeDesc(frame->format()), + frame->const_data(), + OIIO::AutoStride, + frame->linesize_bytes()); + + // Create a destination OIIO buffer with our destination format + OIIO::ImageBuf dst(OIIO::ImageSpec(converted->width(), + converted->height(), + ChannelCount(converted->format()), + GetOIIOTypeDesc(converted->format()))); if (dst.copy_pixels(src)) { + + // Convert our buffer back to a frame + OIIODecoder::BufferToFrame(&dst, converted); + return converted; } else { return nullptr; diff --git a/app/shaders/blur.frag b/app/shaders/blur.frag index 1e520a996..65092c9cf 100644 --- a/app/shaders/blur.frag +++ b/app/shaders/blur.frag @@ -29,7 +29,7 @@ out vec4 fragColor; // Double gaussian formula, actually used in the code below // Should be faster than the single gaussian above since it doesn't need sqrt() float gaussian2(float x, float y, float sigma) { - return (1.0/(pow(sigma, 2.0)*2.0*M_PI))*exp(-0.5*((pow(x, 2.0) + pow(y, 2.0))/pow(sigma, 2.0))); + return (1.0/((sigma*sigma)*2.0*M_PI))*exp(-0.5*(((x*x) + (y*y))/(sigma*sigma))); } void main(void) { diff --git a/app/shaders/rgbwaveform.frag b/app/shaders/rgbwaveform.frag index c97562daa..3e839b386 100644 --- a/app/shaders/rgbwaveform.frag +++ b/app/shaders/rgbwaveform.frag @@ -5,8 +5,13 @@ uniform sampler2D ove_maintex; uniform vec2 ove_resolution; +uniform vec2 ove_viewport; +uniform vec3 luma_coeffs; -uniform float threshold; +uniform float waveform_scale; +uniform vec2 waveform_dims; +uniform vec4 waveform_region; +uniform vec4 waveform_uv; in vec2 ove_texcoord; @@ -14,18 +19,53 @@ out vec4 fragColor; void main(void) { vec3 col = vec3(0.0); - float s = ove_texcoord.y * 1.8 - 0.15; - float maxb = s + threshold; - float minb = s - threshold; + // Set an increment default to 10 bit encodings. This would likely be + // better served as a UI control, as waveforms will change their combing + // based on how granular the increment is set. For example, it can be + // challenging to spot 8 bit combing with an increment of 1. / 2.^8 - 1. + float increment = 1.0 / (pow(2, 10) - 1.0); + float maxb = waveform_dims.y + increment; + float minb = waveform_dims.y - increment; - int y_lim = int(ove_resolution.y); + // Intensity would make sense to also expose via the UI, as a density + // slider allows you to peek past certain values or reveal very low + // values. Hard coding it for now, as there isn't a clear way to have + // the various bit depth / code values always display at a consistent + // emission output strength. + float intensity = 0.10; - for (int i = 0; i < y_lim; i++) { - vec3 x = texture(ove_maintex, vec2(ove_texcoord.x, float(i) / float(ove_resolution.y))).rgb; - col += step(x, vec3(maxb)) * step(vec3(minb), x) / (ove_resolution.y * 0.125); + int y_lim = int(waveform_dims.y); - float l = dot(x, x); - col += step(l, maxb * maxb) * step(minb * minb, l) / (ove_resolution.y * 0.125); + vec3 cur_col = vec3(0.0); + vec3 cur_lum = vec3(0.0); + + if ( + (gl_FragCoord.x >= waveform_region.x) && + (gl_FragCoord.y >= waveform_region.y) && + (gl_FragCoord.x < waveform_region.z) && + (gl_FragCoord.y < waveform_region.w) + ) { + // col = vec3(0.5, 0.5, 0.0); + // int start = int(waveform_region.y); + int stop = int(waveform_dims.y); + float ratio = 0.0; + float waveform_x = (ove_texcoord.x - waveform_uv.x) / waveform_scale; + float waveform_y = (ove_texcoord.y - waveform_uv.y) / waveform_scale; + for (int i = 0; i < waveform_dims.y; i++) { + ratio = float(i) / float(waveform_dims.y); + cur_col = texture( + ove_maintex, + vec2(waveform_x, ratio) + ).rgb; + + col += step(vec3(waveform_y - increment), cur_col) * + step(cur_col, vec3(waveform_y + increment)) * intensity; + + cur_lum = vec3(dot(cur_col, luma_coeffs)); + + col += step(vec3(waveform_y - increment), cur_lum) * + step(cur_lum, vec3(waveform_y + increment)) * intensity; + } } fragColor = vec4(col, 1.0); diff --git a/app/ui/style/olive-dark/palette.ini b/app/ui/style/olive-dark/palette.ini index 91d617459..7f93ffd25 100644 --- a/app/ui/style/olive-dark/palette.ini +++ b/app/ui/style/olive-dark/palette.ini @@ -4,7 +4,6 @@ Base=#191919 BrightText=#FF0000 Button=#353535 ButtonText=#FFFFFF -Disabled-ButtonText=#808080 Highlight=#2A82DA HighlightedText=#FFFFFF Link=#2A82DA @@ -16,3 +15,4 @@ WindowText=#FFFFFF [Disabled] ButtonText=#808080 +Text=#A0A0A0 diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index 71c742619..4cd9dba3e 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -168,6 +168,11 @@ void CurveWidget::SetVerticalScale(const double &vscale) view_->SetYScale(vscale); } +void CurveWidget::DeleteSelected() +{ + view_->DeleteSelected(); +} + void CurveWidget::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { @@ -200,6 +205,8 @@ void CurveWidget::ScaleChangedEvent(const double &scale) void CurveWidget::TimeTargetChangedEvent(Node *target) { + ConnectViewerNode(nullptr); + key_control_->SetTimeTarget(target); view_->SetTimeTarget(target); @@ -207,6 +214,12 @@ void CurveWidget::TimeTargetChangedEvent(Node *target) if (bridge_) { bridge_->SetTimeTarget(target); } + + // FIXME: If a non-viewer node is ever set here, it will fail to update the length + ViewerOutput* viewer = dynamic_cast(target); + if (viewer) { + ConnectViewerNode(viewer); + } } void CurveWidget::UpdateInputLabel() diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index b0276bb7f..21e0400d0 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -47,6 +47,8 @@ public: const double& GetVerticalScale(); void SetVerticalScale(const double& vscale); + void DeleteSelected(); + protected: virtual void changeEvent(QEvent *) override; diff --git a/app/widget/keyframeview/keyframeviewbase.cpp b/app/widget/keyframeview/keyframeviewbase.cpp index 2baac4e4c..65009bef4 100644 --- a/app/widget/keyframeview/keyframeviewbase.cpp +++ b/app/widget/keyframeview/keyframeviewbase.cpp @@ -73,6 +73,25 @@ void KeyframeViewBase::SetYScale(const double &y_scale) } } +void KeyframeViewBase::DeleteSelected() +{ + QUndoCommand* command = new QUndoCommand(); + + QMap::const_iterator i; + + for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) { + if (i.value()->isSelected()) { + NodeInput* input_parent = i.key()->parent(); + + new NodeParamRemoveKeyframeCommand(input_parent, + input_parent->get_keyframe_shared_ptr_from_raw(i.key()), + command); + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); +} + void KeyframeViewBase::RemoveKeyframe(NodeKeyframePtr key) { KeyframeAboutToBeRemoved(key.get()); @@ -391,7 +410,7 @@ void KeyframeViewBase::ShowContextMenu() { Menu m; - MenuShared::instance()->AddItemsForEditMenu(&m); + MenuShared::instance()->AddItemsForEditMenu(&m, false); QAction* linear_key_action = nullptr; QAction* bezier_key_action = nullptr; diff --git a/app/widget/keyframeview/keyframeviewbase.h b/app/widget/keyframeview/keyframeviewbase.h index 71b46f6a7..000394641 100644 --- a/app/widget/keyframeview/keyframeviewbase.h +++ b/app/widget/keyframeview/keyframeviewbase.h @@ -40,6 +40,8 @@ public: const double& GetYScale() const; void SetYScale(const double& y_scale); + void DeleteSelected(); + public slots: void RemoveKeyframe(NodeKeyframePtr key); diff --git a/app/widget/manageddisplay/manageddisplay.cpp b/app/widget/manageddisplay/manageddisplay.cpp index 63cb2dd69..2208dfeb8 100644 --- a/app/widget/manageddisplay/manageddisplay.cpp +++ b/app/widget/manageddisplay/manageddisplay.cpp @@ -32,6 +32,11 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent) : setContextMenuPolicy(Qt::CustomContextMenu); } +ManagedDisplayWidget::~ManagedDisplayWidget() +{ + ContextCleanup(); +} + void ManagedDisplayWidget::ConnectColorManager(ColorManager *color_manager) { if (color_manager_ == color_manager) { @@ -142,9 +147,13 @@ void ManagedDisplayWidget::MenuLookSelect(QAction *action) void ManagedDisplayWidget::SetColorTransform(const ColorTransform &transform) { + makeCurrent(); + color_transform_ = transform; SetupColorProcessor(); ColorProcessorChangedEvent(); + + doneCurrent(); } void ManagedDisplayWidget::initializeGL() @@ -248,9 +257,7 @@ void ManagedDisplayWidget::SetupColorProcessor() color_manager_->GetReferenceColorSpace(), color_transform_); - makeCurrent(); color_service_->Enable(context(), true); - doneCurrent(); } catch (OCIO::Exception& e) { diff --git a/app/widget/manageddisplay/manageddisplay.h b/app/widget/manageddisplay/manageddisplay.h index e0e3e3b3e..697247129 100644 --- a/app/widget/manageddisplay/manageddisplay.h +++ b/app/widget/manageddisplay/manageddisplay.h @@ -35,6 +35,8 @@ class ManagedDisplayWidget : public QOpenGLWidget public: ManagedDisplayWidget(QWidget* parent = nullptr); + virtual ~ManagedDisplayWidget() override; + /** * @brief Disconnect a ColorManager (equivalent to ConnectColorManager(nullptr)) */ diff --git a/app/widget/menu/menu.cpp b/app/widget/menu/menu.cpp index 0ffc65e95..5515c65da 100644 --- a/app/widget/menu/menu.cpp +++ b/app/widget/menu/menu.cpp @@ -50,6 +50,17 @@ Menu::Menu(const QString &s, QWidget *parent) : Init(); } +QAction *Menu::AddActionWithData(const QString &text, const QVariant &data, const QVariant &compare) +{ + QAction* a = addAction(text); + + a->setData(data); + a->setCheckable(true); + a->setChecked(data == compare); + + return a; +} + QAction* Menu::InsertAlphabetically(const QString &s) { QAction* action = new QAction(s, this); diff --git a/app/widget/menu/menu.h b/app/widget/menu/menu.h index 4151cfd79..edafaf85d 100644 --- a/app/widget/menu/menu.h +++ b/app/widget/menu/menu.h @@ -131,6 +131,10 @@ public: return a; } + QAction* AddActionWithData(const QString& text, + const QVariant& data, + const QVariant& compare); + QAction *InsertAlphabetically(const QString& s); void InsertAlphabetically(QAction* entry); void InsertAlphabetically(Menu* menu); diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index 96015d2b8..563630fb7 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -79,7 +79,7 @@ void MenuShared::AddItemsForNewMenu(Menu *m) m->addAction(new_folder_item_); } -void MenuShared::AddItemsForEditMenu(Menu *m) +void MenuShared::AddItemsForEditMenu(Menu *m, bool for_clips) { m->addAction(edit_cut_item_); m->addAction(edit_copy_item_); @@ -87,8 +87,11 @@ void MenuShared::AddItemsForEditMenu(Menu *m) m->addAction(edit_paste_insert_item_); m->addAction(edit_duplicate_item_); m->addAction(edit_delete_item_); - m->addAction(edit_ripple_delete_item_); - m->addAction(edit_split_item_); + + if (for_clips) { + m->addAction(edit_ripple_delete_item_); + m->addAction(edit_split_item_); + } } void MenuShared::AddItemsForInOutMenu(Menu *m) @@ -185,7 +188,7 @@ void MenuShared::PasteInsertTriggered() void MenuShared::DuplicateTriggered() { - qDebug() << "FIXME: Stub"; + PanelManager::instance()->CurrentlyFocused()->Duplicate(); } void MenuShared::EnableDisableTriggered() diff --git a/app/widget/menu/menushared.h b/app/widget/menu/menushared.h index 81848b3db..684616610 100644 --- a/app/widget/menu/menushared.h +++ b/app/widget/menu/menushared.h @@ -39,7 +39,7 @@ public: void Retranslate(); void AddItemsForNewMenu(Menu* m); - void AddItemsForEditMenu(Menu* m); + void AddItemsForEditMenu(Menu* m, bool for_clips); void AddItemsForInOutMenu(Menu* m); void AddItemsForClipEditMenu(Menu* m); diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 648d8c308..eaae8d0eb 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -158,7 +158,9 @@ void NodeParamView::SetNodes(QList nodes) items_.append(item); - QMetaObject::invokeMethod(item, "SignalAllKeyframes", Qt::QueuedConnection); + QMetaObject::invokeMethod(item, + "SignalAllKeyframes", + Qt::QueuedConnection); emit OpenedNode(node); @@ -225,6 +227,16 @@ const QList &NodeParamView::nodes() return nodes_; } +Node *NodeParamView::GetTimeTarget() const +{ + return keyframe_view_->GetTimeTarget(); +} + +void NodeParamView::DeleteSelected() +{ + keyframe_view_->DeleteSelected(); +} + void NodeParamView::UpdateItemTime(const int64_t ×tamp) { rational time = Timecode::timestamp_to_time(timestamp, keyframe_view_->timebase()); diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index 0a34a4bfc..fd6fe4220 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -40,6 +40,10 @@ public: void SetNodes(QList nodes); const QList& nodes(); + Node* GetTimeTarget() const; + + void DeleteSelected(); + signals: void InputDoubleClicked(NodeInput* input); diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 6d7a2f0ee..326f674b3 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -73,6 +73,8 @@ NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : connect(title_bar_collapse_btn_, &QPushButton::toggled, body_, &NodeParamViewItemBody::setVisible); main_layout->addWidget(body_); + connect(node_, &Node::LabelChanged, this, &NodeParamViewItem::Retranslate); + Retranslate(); } @@ -111,7 +113,11 @@ void NodeParamViewItem::Retranslate() { node_->Retranslate(); - title_bar_lbl_->setText(node_->Name()); + if (node_->GetLabel().isEmpty()) { + title_bar_lbl_->setText(node_->Name()); + } else { + title_bar_lbl_->setText(tr("%1 (%2)").arg(node_->GetLabel(), node_->Name())); + } body_->Retranslate(); } diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index 5209d68ed..6d6996219 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -129,8 +129,6 @@ protected: virtual void changeEvent(QEvent *e) override; private: - void Retranslate(); - NodeParamViewItemTitleBar* title_bar_; QLabel* title_bar_lbl_; @@ -143,6 +141,9 @@ private: rational time_; +private slots: + void Retranslate(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeview/CMakeLists.txt b/app/widget/nodeview/CMakeLists.txt index 9682e3972..c36c19882 100644 --- a/app/widget/nodeview/CMakeLists.txt +++ b/app/widget/nodeview/CMakeLists.txt @@ -18,6 +18,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} widget/nodeview/nodeview.h widget/nodeview/nodeview.cpp + widget/nodeview/nodeviewcommon.h widget/nodeview/nodeviewedge.h widget/nodeview/nodeviewedge.cpp widget/nodeview/nodeviewitem.h diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 5483d50cf..3e9653bd1 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -20,30 +20,34 @@ #include "nodeview.h" +#include #include #include "core.h" #include "nodeviewundo.h" #include "node/factory.h" +#include "widget/menu/menushared.h" + +#define super HandMovableView OLIVE_NAMESPACE_ENTER NodeView::NodeView(QWidget *parent) : - QGraphicsView(parent), + HandMovableView(parent), graph_(nullptr), - attached_item_(nullptr), drop_edge_(nullptr) { setScene(&scene_); setDragMode(RubberBandDrag); setContextMenuPolicy(Qt::CustomContextMenu); + setMouseTracking(true); + setRenderHint(QPainter::Antialiasing); connect(&scene_, &QGraphicsScene::changed, this, &NodeView::ItemsChanged); connect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::SceneSelectionChangedSlot); connect(this, &NodeView::customContextMenuRequested, this, &NodeView::ShowContextMenu); - setMouseTracking(true); - setRenderHint(QPainter::Antialiasing); + SetFlowDirection(NodeViewCommon::kTopToBottom); } NodeView::~NodeView() @@ -93,21 +97,33 @@ void NodeView::DeleteSelected() return; } - QList selected_nodes = scene_.GetSelectedNodes(); + QUndoCommand* command = new QUndoCommand(); - // Ensure no nodes are "undeletable" - for (int i=0;iCanBeDeleted()) { - selected_nodes.removeAt(i); - i--; + { + QList selected_edges = scene_.GetSelectedEdges(); + + foreach (NodeEdge* edge, selected_edges) { + new NodeEdgeRemoveCommand(edge->output(), edge->input(), command); } } - if (selected_nodes.isEmpty()) { - return; + { + QList selected_nodes = scene_.GetSelectedNodes(); + + // Ensure no nodes are "undeletable" + for (int i=0;iCanBeDeleted()) { + selected_nodes.removeAt(i); + i--; + } + } + + if (!selected_nodes.isEmpty()) { + new NodeRemoveCommand(graph_, selected_nodes, command); + } } - Core::instance()->undo_stack()->push(new NodeRemoveCommand(graph_, selected_nodes)); + Core::instance()->undo_stack()->pushIfHasChildren(command); } void NodeView::SelectAll() @@ -181,7 +197,60 @@ void NodeView::Paste() Core::instance()->undo_stack()->pushIfHasChildren(command); if (!pasted_nodes.isEmpty()) { - // FIXME: Attach to cursor so user can drop in place + AttachNodesToCursor(pasted_nodes); + } +} + +void NodeView::Duplicate() +{ + if (!graph_) { + return; + } + + QList selected = scene_.GetSelectedNodes(); + + if (selected.isEmpty()) { + return; + } + + QUndoCommand* command = new QUndoCommand(); + + QList duplicated_nodes; + + foreach (Node* n, selected) { + Node* copy = n->copy(); + + Node::CopyInputs(n, copy, false); + + duplicated_nodes.append(copy); + + new NodeAddCommand(graph_, copy, command); + } + + for (int i=0;ioutput()->edges()) { + if (edge->input()->parentNode() == dst) { + new NodeEdgeAddCommand(duplicated_nodes.at(i)->output(), + duplicated_nodes.at(j)->GetInputWithID(edge->input()->id()), + command); + } + } + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); + + if (!duplicated_nodes.isEmpty()) { + AttachNodesToCursor(duplicated_nodes); } } @@ -196,10 +265,10 @@ void NodeView::ItemsChanged() void NodeView::keyPressEvent(QKeyEvent *event) { - QGraphicsView::keyPressEvent(event); + super::keyPressEvent(event); - if (event->key() == Qt::Key_Escape && attached_item_) { - DetachItemFromCursor(); + if (event->key() == Qt::Key_Escape && !attached_items_.isEmpty()) { + DetachItemsFromCursor(); // We undo the last action which SHOULD be adding the node // FIXME: Possible danger of this not being the case? @@ -209,93 +278,111 @@ void NodeView::keyPressEvent(QKeyEvent *event) void NodeView::mousePressEvent(QMouseEvent *event) { - if (attached_item_) { - Node* dropping_node = attached_item_->GetNode(); + if (HandPress(event)) return; - DetachItemFromCursor(); + if (!attached_items_.isEmpty()) { + if (attached_items_.size() == 1) { + Node* dropping_node = attached_items_.first().item->GetNode(); - if (drop_edge_) { - NodeEdgePtr old_edge = drop_edge_->edge(); + if (drop_edge_) { + NodeEdgePtr old_edge = drop_edge_->edge(); - // We have everything we need to place the node in between - QUndoCommand* command = new QUndoCommand(); + // We have everything we need to place the node in between + QUndoCommand* command = new QUndoCommand(); - // Remove old edge - new NodeEdgeRemoveCommand(old_edge, command); + // Remove old edge + new NodeEdgeRemoveCommand(old_edge, command); - // Place new edges - new NodeEdgeAddCommand(old_edge->output(), drop_compatible_input_, command); - new NodeEdgeAddCommand(dropping_node->output(), old_edge->input(), command); + // Place new edges + new NodeEdgeAddCommand(old_edge->output(), drop_input_, command); + new NodeEdgeAddCommand(dropping_node->output(), old_edge->input(), command); - Core::instance()->undo_stack()->push(command); + Core::instance()->undo_stack()->push(command); + } + + drop_edge_ = nullptr; } - drop_edge_ = nullptr; + DetachItemsFromCursor(); } - QGraphicsView::mousePressEvent(event); + super::mousePressEvent(event); } void NodeView::mouseMoveEvent(QMouseEvent *event) { - QGraphicsView::mouseMoveEvent(event); + if (HandMove(event)) return; - if (attached_item_) { - attached_item_->setPos(mapToScene(event->pos())); + super::mouseMoveEvent(event); - // See if the user clicked on an edge - QRect edge_detect_rect(event->pos(), event->pos()); + if (!attached_items_.isEmpty()) { + MoveAttachedNodesToCursor(event->pos()); - // FIXME: Hardcoded numbers - edge_detect_rect.adjust(-20, -20, 20, 20); + // See if the user clicked on an edge (only when dropping single nodes) + if (attached_items_.size() == 1) { + Node* attached_node = attached_items_.first().item->GetNode(); - QList items = this->items(edge_detect_rect); + QRect edge_detect_rect(event->pos(), event->pos()); - NodeViewEdge* new_drop_edge = nullptr; + // FIXME: Hardcoded numbers + edge_detect_rect.adjust(-20, -20, 20, 20); - foreach (QGraphicsItem* item, items) { - NodeViewEdge* edge = dynamic_cast(item); + QList items = this->items(edge_detect_rect); - if (edge) { - // Try to place this node inside this edge + NodeViewEdge* new_drop_edge = nullptr; - // See if the node we're dropping has an input of a compatible data type - NodeInput* edges_input = edge->edge()->input(); - NodeParam::DataType input_type = edges_input->data_type(); + // See if there is an edge here + foreach (QGraphicsItem* item, items) { + new_drop_edge = dynamic_cast(item); - NodeInput* compatible_input = nullptr; + if (new_drop_edge) { + drop_input_ = nullptr; - foreach (NodeParam* drop_node_param, attached_item_->GetNode()->parameters()) { - if (drop_node_param->type() == NodeParam::kInput - && static_cast(drop_node_param)->data_type() & input_type) { - compatible_input = static_cast(drop_node_param); + foreach (NodeParam* param, attached_node->parameters()) { + if (param->type() == NodeParam::kInput) { + NodeInput* input = static_cast(param); + + if (input->IsConnectable()) { + if (input->data_type() & new_drop_edge->edge()->input()->data_type()) { + drop_input_ = input; + break; + } else if (!drop_input_) { + drop_input_ = input; + } + } + } + } + + if (drop_input_) { break; + } else { + new_drop_edge = nullptr; } } + } - if (compatible_input) { - new_drop_edge = edge; - drop_compatible_input_ = compatible_input; - - break; + if (drop_edge_ != new_drop_edge) { + if (drop_edge_) { + drop_edge_->SetHighlighted(false); } - } - } - if (drop_edge_ != new_drop_edge) { - if (drop_edge_) { - drop_edge_->SetHighlighted(false); - } + drop_edge_ = new_drop_edge; - drop_edge_ = new_drop_edge; - - if (drop_edge_) { - drop_edge_->SetHighlighted(true); + if (drop_edge_) { + drop_edge_->SetHighlighted(true); + } } } } } +void NodeView::mouseReleaseEvent(QMouseEvent *event) +{ + if (HandRelease(event)) return; + + super::mouseReleaseEvent(event); +} + void NodeView::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ControlModifier) { @@ -321,10 +408,59 @@ void NodeView::ShowContextMenu(const QPoint &pos) Menu m; - Menu* add_menu = NodeFactory::CreateMenu(&m); - add_menu->setTitle(tr("Add")); - connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot); - m.addMenu(add_menu); + MenuShared::instance()->AddItemsForEditMenu(&m, false); + + m.addSeparator(); + + QList selected = scene_.GetSelectedItems(); + + if (itemAt(pos) && !selected.isEmpty()) { + + if (selected.size() == 1) { + + // Label node action + QAction* label_action = m.addAction(tr("Label")); + connect(label_action, &QAction::triggered, this, &NodeView::ContextMenuLabelNode); + + m.addSeparator(); + + } + + // Auto-position action + QAction* autopos = m.addAction(tr("Auto-Position")); + connect(autopos, &QAction::triggered, this, &NodeView::AutoPositionDescendents); + + } else { + + Menu* direction_menu = new Menu(tr("Direction"), &m); + m.addMenu(direction_menu); + + direction_menu->AddActionWithData(tr("Top to Bottom"), + NodeViewCommon::kTopToBottom, + scene_.GetFlowDirection()); + + direction_menu->AddActionWithData(tr("Bottom to Top"), + NodeViewCommon::kBottomToTop, + scene_.GetFlowDirection()); + + direction_menu->AddActionWithData(tr("Left to Right"), + NodeViewCommon::kLeftToRight, + scene_.GetFlowDirection()); + + direction_menu->AddActionWithData(tr("Right to Left"), + NodeViewCommon::kRightToLeft, + scene_.GetFlowDirection()); + + connect(direction_menu, &Menu::triggered, this, &NodeView::ContextMenuSetDirection); + + m.addSeparator(); + + Menu* add_menu = NodeFactory::CreateMenu(&m); + add_menu->setTitle(tr("Add")); + connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot); + m.addMenu(add_menu); + + } m.exec(mapToGlobal(pos)); } @@ -337,7 +473,45 @@ void NodeView::CreateNodeSlot(QAction *action) Core::instance()->undo_stack()->push(new NodeAddCommand(graph_, new_node)); NodeViewItem* item = scene_.NodeToUIObject(new_node); - AttachItemToCursor(item); + AttachItemsToCursor({item}); + } +} + +void NodeView::ContextMenuSetDirection(QAction *action) +{ + SetFlowDirection(static_cast(action->data().toInt())); +} + +void NodeView::AutoPositionDescendents() +{ + QList selected = scene_.GetSelectedNodes(); + + foreach (Node* n, selected) { + scene_.ReorganizeFrom(n); + } +} + +void NodeView::ContextMenuLabelNode() +{ + QList nodes = scene_.GetSelectedNodes(); + + if (nodes.isEmpty()) { + return; + } + + Node* n = nodes.first(); + + bool ok; + + QString s = QInputDialog::getText(this, + tr("Label Node"), + tr("Set node label"), + QLineEdit::Normal, + n->GetLabel(), + &ok); + + if (ok) { + n->SetLabel(s); } } @@ -475,16 +649,50 @@ void NodeView::PlaceNode(NodeViewItem *n, const QPointF &pos) } } -void NodeView::AttachItemToCursor(NodeViewItem *item) +void NodeView::AttachNodesToCursor(const QList &nodes) { - attached_item_ = item; + QList items; - setMouseTracking(attached_item_); + foreach (Node* p, nodes) { + items.append(scene_.NodeToUIObject(p)); + } + + AttachItemsToCursor(items); } -void NodeView::DetachItemFromCursor() +void NodeView::AttachItemsToCursor(const QList& items) { - AttachItemToCursor(nullptr); + DetachItemsFromCursor(); + + if (!items.isEmpty()) { + foreach (NodeViewItem* i, items) { + attached_items_.append({i, i->pos() - items.first()->pos()}); + } + + setMouseTracking(true); + + MoveAttachedNodesToCursor(mapFromGlobal(QCursor::pos())); + } +} + +void NodeView::DetachItemsFromCursor() +{ + attached_items_.clear(); + setMouseTracking(false); +} + +void NodeView::SetFlowDirection(NodeViewCommon::FlowDirection dir) +{ + scene_.SetFlowDirection(dir); +} + +void NodeView::MoveAttachedNodesToCursor(const QPoint& p) +{ + QPointF item_pos = mapToScene(p); + + foreach (const AttachedItem& i, attached_items_) { + i.item->setPos(item_pos + i.original_pos); + } } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 05d72464e..2701f4a1d 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -26,6 +26,7 @@ #include "node/graph.h" #include "nodeviewscene.h" +#include "widget/timelinewidget/view/handmovableview.h" #include "widget/nodecopypaste/nodecopypaste.h" OLIVE_NAMESPACE_ENTER @@ -36,7 +37,7 @@ OLIVE_NAMESPACE_ENTER * This widget takes a NodeGraph object and constructs a QGraphicsScene representing its data, viewing and allowing * the user to make modifications to it. */ -class NodeView : public QGraphicsView, public NodeCopyPasteWidget +class NodeView : public HandMovableView, public NodeCopyPasteWidget { Q_OBJECT public: @@ -63,6 +64,8 @@ public: void CopySelected(bool cut); void Paste(); + void Duplicate(); + signals: /** * @brief Signal emitted when the selected nodes have changed @@ -73,24 +76,35 @@ protected: virtual void keyPressEvent(QKeyEvent *event) override; virtual void mousePressEvent(QMouseEvent *event) override; - virtual void mouseMoveEvent(QMouseEvent *event) override; + virtual void mouseReleaseEvent(QMouseEvent* event) override; virtual void wheelEvent(QWheelEvent* event) override; private: void PlaceNode(NodeViewItem* n, const QPointF& pos); - void AttachItemToCursor(NodeViewItem* item); + void AttachNodesToCursor(const QList& nodes); - void DetachItemFromCursor(); + void AttachItemsToCursor(const QList& items); + + void DetachItemsFromCursor(); + + void SetFlowDirection(NodeViewCommon::FlowDirection dir); + + void MoveAttachedNodesToCursor(const QPoint &p); NodeGraph* graph_; - NodeViewItem* attached_item_; + struct AttachedItem { + NodeViewItem* item; + QPointF original_pos; + }; + + QList attached_items_; NodeViewEdge* drop_edge_; - NodeInput* drop_compatible_input_; + NodeInput* drop_input_; NodeViewScene scene_; @@ -117,6 +131,21 @@ private slots: */ void CreateNodeSlot(QAction* action); + /** + * @brief Receiver for setting the direction from the context menu + */ + void ContextMenuSetDirection(QAction* action); + + /** + * @brief Receiver for auto-position descendents menu action + */ + void AutoPositionDescendents(); + + /** + * @brief Receiver for labelling a node from the context menu + */ + void ContextMenuLabelNode(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeview/nodeviewcommon.h b/app/widget/nodeview/nodeviewcommon.h new file mode 100644 index 000000000..cc2b870fe --- /dev/null +++ b/app/widget/nodeview/nodeviewcommon.h @@ -0,0 +1,58 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODEVIEWCOMMON_H +#define NODEVIEWCOMMON_H + +#include + +#include "common/define.h" + +OLIVE_NAMESPACE_ENTER + +class NodeViewCommon { +public: + enum FlowDirection { + kTopToBottom, + kBottomToTop, + kLeftToRight, + kRightToLeft + }; + + static Qt::Orientation GetFlowOrientation(FlowDirection dir) { + if (dir == kTopToBottom || dir == kBottomToTop) { + return Qt::Vertical; + } else { + return Qt::Horizontal; + } + } + + static bool DirectionsAreOpposing(FlowDirection a, FlowDirection b) { + return ((a == NodeViewCommon::kLeftToRight && b == NodeViewCommon::kRightToLeft) + || (a == NodeViewCommon::kRightToLeft && b == NodeViewCommon::kLeftToRight) + || (a == NodeViewCommon::kTopToBottom && b == NodeViewCommon::kBottomToTop) + || (a == NodeViewCommon::kBottomToTop && b == NodeViewCommon::kTopToBottom)); + } + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODEVIEWCOMMON_H diff --git a/app/widget/nodeview/nodeviewedge.cpp b/app/widget/nodeview/nodeviewedge.cpp index 333385e22..205785cbf 100644 --- a/app/widget/nodeview/nodeviewedge.cpp +++ b/app/widget/nodeview/nodeviewedge.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "common/clamp.h" #include "common/lerp.h" @@ -34,9 +35,12 @@ OLIVE_NAMESPACE_ENTER NodeViewEdge::NodeViewEdge(QGraphicsItem *parent) : QGraphicsPathItem(parent), edge_(nullptr), - color_group_(QPalette::Active), - color_role_(QPalette::Text) + connected_(false), + highlighted_(false), + flow_dir_(NodeViewCommon::kLeftToRight) { + setFlag(QGraphicsItem::ItemIsSelectable); + // Ensures this UI object is drawn behind other objects setZValue(-1); @@ -75,45 +79,79 @@ void NodeViewEdge::Adjust() } // Draw a line between the two - SetPoints(output->GetParamPoint(edge_->output()), input->GetParamPoint(edge_->input())); + SetPoints(output->GetParamPoint(edge_->output(), output->pos()), + input->GetParamPoint(edge_->input(), output->pos()), + input->IsExpanded()); } void NodeViewEdge::SetConnected(bool c) { - if (c) { - color_group_ = QPalette::Active; - } else { - color_group_ = QPalette::Disabled; - } + connected_ = c; - UpdatePen(); + update(); } void NodeViewEdge::SetHighlighted(bool e) { - if (e) { - color_role_ = QPalette::Highlight; - } else { - color_role_ = QPalette::Text; - } + highlighted_ = e; - UpdatePen(); + update(); } -void NodeViewEdge::SetPoints(const QPointF &start, const QPointF &end) +void NodeViewEdge::SetPoints(const QPointF &start, const QPointF &end, bool input_is_expanded) { QPainterPath path; - double half_x = lerp(start.x(), end.x(), 0.5); path.moveTo(start); - path.cubicTo(QPointF(half_x, start.y()), QPointF(half_x, end.y()), end); + + double half_x = lerp(start.x(), end.x(), 0.5); + double half_y = lerp(start.y(), end.y(), 0.5); + + QPointF cp1, cp2; + + if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + cp1 = QPointF(half_x, start.y()); + } else { + cp1 = QPointF(start.x(), half_y); + } + + if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal || input_is_expanded) { + cp2 = QPointF(half_x, end.y()); + } else { + cp2 = QPointF(end.x(), half_y); + } + + path.cubicTo(cp1, cp2, end); + setPath(path); } -void NodeViewEdge::UpdatePen() +void NodeViewEdge::SetFlowDirection(NodeViewCommon::FlowDirection dir) { - setPen(QPen(qApp->palette().color(color_group_, color_role_), edge_width_)); + flow_dir_ = dir; - //update(); + Adjust(); +} + +void NodeViewEdge::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) +{ + QPalette::ColorGroup group; + QPalette::ColorRole role; + + if (connected_) { + group = QPalette::Active; + } else { + group = QPalette::Disabled; + } + + if (highlighted_ != bool(option->state & QStyle::State_Selected)) { + role = QPalette::Highlight; + } else { + role = QPalette::Text; + } + + painter->setPen(QPen(qApp->palette().color(group, role), edge_width_)); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path()); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeview/nodeviewedge.h b/app/widget/nodeview/nodeviewedge.h index 1f9b66876..045955a05 100644 --- a/app/widget/nodeview/nodeviewedge.h +++ b/app/widget/nodeview/nodeviewedge.h @@ -25,6 +25,7 @@ #include #include "node/edge.h" +#include "nodeviewcommon.h" OLIVE_NAMESPACE_ENTER @@ -81,18 +82,26 @@ public: /** * @brief Set points to create curve from */ - void SetPoints(const QPointF& start, const QPointF& end); + void SetPoints(const QPointF& start, const QPointF& end, bool input_is_expanded); + + /** + * @brief Sets the direction nodes are flowing + */ + void SetFlowDirection(NodeViewCommon::FlowDirection dir); + +protected: + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; private: - void UpdatePen(); - NodeEdgePtr edge_; int edge_width_; - QPalette::ColorGroup color_group_; + bool connected_; - QPalette::ColorRole color_role_; + bool highlighted_; + + NodeViewCommon::FlowDirection flow_dir_; }; diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index fe42d818d..45eafa7e2 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -44,9 +44,11 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : cached_drop_item_(nullptr), cached_drop_item_expanded_(false), expanded_(false), + hide_titlebar_(false), standard_click_(false), highlighted_index_(-1), - node_edge_change_command_(nullptr) + node_edge_change_command_(nullptr), + flow_dir_(NodeViewCommon::kLeftToRight) { // Set flags for this widget setFlag(QGraphicsItem::ItemIsMovable); @@ -57,26 +59,110 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : // We use font metrics to set all the UI measurements for DPI-awareness // - QFont default_font; - QFontMetrics font_metrics(default_font); - // Set border width - node_border_width_ = font_metrics.height() / 12; + node_border_width_ = DefaultItemBorder(); - // Set text and icon padding - int node_text_padding = font_metrics.height() / 4; - - // Not particularly great way of using text scaling to set the width (DPI-awareness, etc.) - int widget_width = QFontMetricsWidth(font_metrics, "HHHHHHHHHHHHHH"); - - // Use the current default font height to size this widget - // Set default "collapsed" size - int widget_height = font_metrics.height() + node_text_padding * 2; + int widget_width = DefaultItemWidth(); + int widget_height = DefaultItemHeight(); title_bar_rect_ = QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height); setRect(title_bar_rect_); } +QPointF NodeViewItem::GetNodePosition() const +{ + QPointF node_pos; + + qreal adjusted_x = pos().x() / DefaultItemHorizontalPadding(); + qreal adjusted_y = pos().y() / DefaultItemVerticalPadding(); + + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + node_pos.setX(adjusted_x); + node_pos.setY(adjusted_y); + break; + case NodeViewCommon::kRightToLeft: + node_pos.setX(-adjusted_x); + node_pos.setY(adjusted_y); + break; + case NodeViewCommon::kTopToBottom: + node_pos.setX(adjusted_y); + node_pos.setY(adjusted_x); + break; + case NodeViewCommon::kBottomToTop: + node_pos.setX(-adjusted_y); + node_pos.setY(adjusted_x); + break; + } + + return node_pos; +} + +void NodeViewItem::SetNodePosition(const QPointF &pos) +{ + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + setPos(pos.x() * DefaultItemHorizontalPadding(), + pos.y() * DefaultItemVerticalPadding()); + break; + case NodeViewCommon::kRightToLeft: + setPos(-pos.x() * DefaultItemHorizontalPadding(), + pos.y() * DefaultItemVerticalPadding()); + break; + case NodeViewCommon::kTopToBottom: + setPos(pos.y() * DefaultItemHorizontalPadding(), + pos.x() * DefaultItemVerticalPadding()); + break; + case NodeViewCommon::kBottomToTop: + setPos(pos.y() * DefaultItemHorizontalPadding(), + -pos.x() * DefaultItemVerticalPadding()); + break; + } +} + +int NodeViewItem::DefaultTextPadding() +{ + return QFontMetrics(QFont()).height() / 4; +} + +int NodeViewItem::DefaultItemHeight() +{ + return QFontMetrics(QFont()).height() + DefaultTextPadding() * 2; +} + +int NodeViewItem::DefaultItemWidth() +{ + return QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHHHH");; +} + +int NodeViewItem::DefaultMaximumTextWidth() +{ + return QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHH");; +} + +int NodeViewItem::DefaultItemBorder() +{ + return QFontMetrics(QFont()).height() / 12; +} + +qreal NodeViewItem::DefaultItemHorizontalPadding() const +{ + if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + return DefaultItemWidth() * 1.5; + } else { + return DefaultItemWidth() * 1.25; + } +} + +qreal NodeViewItem::DefaultItemVerticalPadding() const +{ + if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + return DefaultItemHeight() * 1.5; + } else { + return DefaultItemHeight() * 2.0; + } +} + void NodeViewItem::SetNode(Node *n) { node_ = n; @@ -96,7 +182,7 @@ void NodeViewItem::SetNode(Node *n) } } - setPos(node_->GetPosition()); + SetNodePosition(node_->GetPosition()); } update(); @@ -112,18 +198,26 @@ bool NodeViewItem::IsExpanded() const return expanded_; } -void NodeViewItem::SetExpanded(bool e) +void NodeViewItem::SetExpanded(bool e, bool hide_titlebar) { - if (expanded_ == e) { + if (node_inputs_.isEmpty() + || (expanded_ == e && hide_titlebar_ == hide_titlebar)) { return; } expanded_ = e; + hide_titlebar_ = hide_titlebar; if (expanded_ && !node_inputs_.isEmpty()) { // Create new rect QRectF new_rect = title_bar_rect_; - new_rect.setHeight(new_rect.height() * node_inputs_.size()); + + if (hide_titlebar_) { + new_rect.setHeight(new_rect.height() * node_inputs_.size()); + } else { + new_rect.setHeight(new_rect.height() * (node_inputs_.size() + 1)); + } + setRect(new_rect); } else { setRect(title_bar_rect_); @@ -143,33 +237,14 @@ void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti // don't want here) QPalette app_pal = Core::instance()->main_window()->palette(); - { - QPen border_pen; - border_pen.setWidth(node_border_width_); - - QBrush bkg_color; - - if (option->state & QStyle::State_Selected) { - border_pen.setColor(app_pal.color(QPalette::Highlight)); - } else { - border_pen.setColor(css_proxy_.BorderColor()); - } - - if (IsExpanded()) { - bkg_color = app_pal.color(QPalette::Window); - } else { - bkg_color = css_proxy_.TitleBarColor(); - } - - painter->setPen(border_pen); - painter->setBrush(bkg_color); + // Draw background rect if expanded + if (IsExpanded()) { + painter->setPen(Qt::NoPen); + painter->setBrush(app_pal.color(QPalette::Window)); painter->drawRect(rect()); - } - painter->setPen(app_pal.color(QPalette::Text)); - - if (IsExpanded()) { + painter->setPen(app_pal.color(QPalette::Text)); for (int i=0;idrawText(input_rect, Qt::AlignCenter, node_inputs_.at(i)->name()); } + } - } else if (node_) { + // Draw the titlebar + if (!hide_titlebar_ && node_) { + + painter->setPen(Qt::black); + painter->setBrush(css_proxy_.TitleBarColor()); + + painter->drawRect(title_bar_rect_); + + painter->setPen(app_pal.color(QPalette::Text)); + + QString node_label; + + if (node_->GetLabel().isEmpty()) { + node_label = node_->ShortName(); + } else { + node_label = node_->GetLabel(); + } + + { + QFont f; + QFontMetrics fm(f); + + int max_text_width = DefaultMaximumTextWidth(); + + if (QFontMetricsWidth(fm, node_label) > max_text_width) { + QString concatenated; + + do { + node_label.chop(1); + concatenated = QCoreApplication::translate("NodeViewItem", "%1...").arg(node_label); + } while (QFontMetricsWidth(fm, concatenated) > max_text_width); + + node_label = concatenated; + } + } // Draw the text in a rect (the rect is sized around text already in the constructor) - painter->drawText(title_bar_rect_, Qt::AlignCenter, node_->Name()); + painter->drawText(title_bar_rect_, + Qt::AlignCenter, + node_label); } + + // Draw final border + QPen border_pen; + border_pen.setWidth(node_border_width_); + + if (option->state & QStyle::State_Selected) { + border_pen.setColor(app_pal.color(QPalette::Highlight)); + } else { + border_pen.setColor(css_proxy_.BorderColor()); + } + + painter->setPen(border_pen); + painter->setBrush(Qt::NoBrush); + + painter->drawRect(rect()); } void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) @@ -203,6 +330,7 @@ void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) // Create draggable object dragging_edge_ = new NodeViewEdge(); + dragging_edge_->SetFlowDirection(flow_dir_); // Set up a QUndoCommand to make this action undoable node_edge_change_command_ = new QUndoCommand(); @@ -213,7 +341,7 @@ void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) drag_src_param_ = param; // Set the starting position to the current param's connector - dragging_edge_start_ = GetParamPoint(param); + dragging_edge_start_ = GetParamPoint(param, QPointF()); } else if (param->type() == NodeParam::kInput) { // For an input param, we default to moving an existing edge @@ -229,8 +357,8 @@ void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) drag_source_ = static_cast(scene())->NodeToUIObject(drag_src_param_->parentNode()); // Get the opposing parameter's rect center using the line's current coordinates - // (we use the current coordinates because a complex formula is used for the line's coords if the opposing - // node is collapsed, therefore it's easier to just retrieve it from line itself) + // (we use the current coordinates because a complex formula is used for the line's coords if + // the opposing node is collapsed, therefore it's easier to just retrieve it from line itself) NodeViewEdge* existing_edge_ui = static_cast(scene())->EdgeToUIObject(edge); QPainterPath existing_edge_line = existing_edge_ui->path(); QPointF edge_start = existing_edge_line.pointAtPercent(0); @@ -288,6 +416,8 @@ void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) cached_drop_item_->SetExpanded(false); } + cached_drop_item_->SetHighlightedIndex(-1); + cached_drop_item_->setZValue(0); cached_drop_item_ = nullptr; } @@ -303,7 +433,7 @@ void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) cached_drop_item_expanded_ = !cached_drop_item_->IsExpanded(); if (cached_drop_item_expanded_) { - cached_drop_item_->SetExpanded(true); + cached_drop_item_->SetExpanded(true, true); } cached_drop_item_->setZValue(1); @@ -330,7 +460,9 @@ void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) if (!cached_drop_item_->GetNode()->OutputsTo(node_)) { drag_dest_param_ = comp_param; highlight_their_index = i; - end_point = cached_drop_item_->mapToScene(cached_drop_item_->GetInputPoint(i)); + + QPointF end_point_local = cached_drop_item_->GetInputPoint(i, pos()); + end_point = cached_drop_item_->mapToScene(end_point_local); } break; @@ -340,9 +472,11 @@ void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) cached_drop_item_->SetHighlightedIndex(highlight_their_index); } - dragging_edge_->SetConnected(drag_dest_param_ != nullptr); + dragging_edge_->SetConnected(drag_dest_param_); - dragging_edge_->SetPoints(dragging_edge_start_, end_point); + dragging_edge_->SetPoints(dragging_edge_start_, + end_point, + cached_drop_item_ ? cached_drop_item_->IsExpanded() : false); return; } @@ -363,8 +497,14 @@ void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) scene()->removeItem(dragging_edge_); // If we expanded an item in the drag, re-collapse it now - if (cached_drop_item_ != nullptr) { - cached_drop_item_->SetExpanded(false); + if (cached_drop_item_) { + if (cached_drop_item_expanded_) { + cached_drop_item_->SetExpanded(false); + } + + cached_drop_item_->SetHighlightedIndex(-1); + cached_drop_item_->setZValue(0); + cached_drop_item_ = nullptr; } @@ -405,10 +545,19 @@ void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } } +void NodeViewItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsRectItem::mouseDoubleClickEvent(event); + + SetExpanded(!IsExpanded()); +} + QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionHasChanged && node_) { - node_->SetPosition(value.toPointF()); + node_->blockSignals(true); + node_->SetPosition(GetNodePosition()); + node_->blockSignals(false); } return QGraphicsItem::itemChange(change, value); @@ -429,6 +578,10 @@ QRectF NodeViewItem::GetInputRect(int index) const { QRectF r = title_bar_rect_; + if (!hide_titlebar_) { + index++; + } + if (IsExpanded()) { r.translate(0, r.height() * index); } @@ -436,10 +589,22 @@ QRectF NodeViewItem::GetInputRect(int index) const return r; } -QPointF NodeViewItem::GetParamPoint(NodeParam *param) const +QPointF NodeViewItem::GetParamPoint(NodeParam *param, const QPointF& source_pos) const { if (param->type() == NodeParam::kOutput) { - return pos() + QPointF(rect().right(), rect().center().y()); + + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + default: + return pos() + QPointF(rect().right(), rect().center().y()); + case NodeViewCommon::kRightToLeft: + return pos() + QPointF(rect().left(), rect().center().y()); + case NodeViewCommon::kTopToBottom: + return pos() + QPointF(rect().center().x(), rect().bottom()); + case NodeViewCommon::kBottomToTop: + return pos() + QPointF(rect().center().x(), rect().top()); + } + } else { NodeInput* input = static_cast(param); @@ -448,15 +613,35 @@ QPointF NodeViewItem::GetParamPoint(NodeParam *param) const input = static_cast(input->parent()); } - return pos() + GetInputPoint(node_inputs_.indexOf(input)); + return pos() + GetInputPoint(node_inputs_.indexOf(input), source_pos); } } -QPointF NodeViewItem::GetInputPoint(int index) const +void NodeViewItem::SetFlowDirection(NodeViewCommon::FlowDirection dir) +{ + flow_dir_ = dir; +} + +QPointF NodeViewItem::GetInputPoint(int index, const QPointF& source_pos) const { QRectF input_rect = GetInputRect(index); - return QPointF(input_rect.left(), input_rect.center().y()); + Qt::Orientation flow_orientation = NodeViewCommon::GetFlowOrientation(flow_dir_); + + if (flow_orientation == Qt::Horizontal || IsExpanded()) { + if (flow_dir_ == NodeViewCommon::kLeftToRight + || (flow_orientation == Qt::Vertical && source_pos.x() < pos().x())) { + return QPointF(input_rect.left(), input_rect.center().y()); + } else { + return QPointF(input_rect.right(), input_rect.center().y()); + } + } else { + if (flow_dir_ == NodeViewCommon::kTopToBottom) { + return QPointF(input_rect.center().x(), input_rect.top()); + } else { + return QPointF(input_rect.center().x(), input_rect.bottom()); + } + } } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index 1ed17f844..0a02b7e3e 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -28,6 +28,7 @@ #include #include "node/node.h" +#include "nodeviewcommon.h" #include "nodeviewedge.h" #include "nodeviewitemwidgetproxy.h" @@ -45,6 +46,9 @@ class NodeViewItem : public QGraphicsRectItem public: NodeViewItem(QGraphicsItem* parent = nullptr); + QPointF GetNodePosition() const; + void SetNodePosition(const QPointF& pos); + /** * @brief Set the Node to correspond to this widget */ @@ -63,13 +67,32 @@ public: /** * @brief Set expanded state */ - void SetExpanded(bool e); + void SetExpanded(bool e, bool hide_titlebar = false); void ToggleExpanded(); /** * @brief Returns GLOBAL point that edges should connect to for any NodeParam member of this object */ - QPointF GetParamPoint(NodeParam* param) const; + QPointF GetParamPoint(NodeParam* param, const QPointF &source_pos) const; + + /** + * @brief Sets the direction nodes are flowing + */ + void SetFlowDirection(NodeViewCommon::FlowDirection dir); + + static int DefaultTextPadding(); + + static int DefaultItemHeight(); + + static int DefaultItemWidth(); + + static int DefaultMaximumTextWidth(); + + static int DefaultItemBorder(); + + qreal DefaultItemHorizontalPadding() const; + + qreal DefaultItemVerticalPadding() const; protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; @@ -77,6 +100,7 @@ protected: virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override; virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; virtual QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override; @@ -94,7 +118,7 @@ private: /** * @brief Returns local point that edges should connect to for a NodeInput in array node_inputs_[index] */ - QPointF GetInputPoint(int index) const; + QPointF GetInputPoint(int index, const QPointF &source_pos) const; /** * @brief Reference to attached Node @@ -136,6 +160,8 @@ private: */ bool expanded_; + bool hide_titlebar_; + /** * @brief Current click mode * @@ -153,6 +179,8 @@ private: */ QUndoCommand* node_edge_change_command_; + NodeViewCommon::FlowDirection flow_dir_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 7de9ce7b4..4b11dd423 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -20,14 +20,40 @@ #include "nodeviewscene.h" +#include "nodeviewedge.h" +#include "nodeviewitem.h" + OLIVE_NAMESPACE_ENTER NodeViewScene::NodeViewScene(QObject *parent) : QGraphicsScene(parent), - graph_(nullptr) + graph_(nullptr), + direction_(NodeViewCommon::kLeftToRight) { - connect(&reorganize_timer_, &QTimer::timeout, &reorganize_timer_, &QTimer::stop); - connect(&reorganize_timer_, &QTimer::timeout, this, &NodeViewScene::Reorganize); +} + +void NodeViewScene::SetFlowDirection(NodeViewCommon::FlowDirection direction) +{ + direction_ = direction; + + { + // Iterate over node items setting direction + QHash::const_iterator i; + for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) { + i.value()->SetFlowDirection(direction_); + + // Update position too + i.value()->SetNodePosition(i.key()->GetPosition()); + } + } + + { + // Iterate over edge items setting direction + QHash::const_iterator i; + for (i=edge_map_.constBegin(); i!=edge_map_.constEnd(); i++) { + i.value()->SetFlowDirection(direction_); + } + } } void NodeViewScene::clear() @@ -35,7 +61,7 @@ void NodeViewScene::clear() // Deselect everything (prevents signals that a selection has changed after deleting an object) DeselectAll(); - // HACK: QGraphicsScene contains some sort of internal hashing of the selected items which doesn't update unless + // HACK: QGraphicsScene contains some sort of internal caching of the selected items which doesn't update unless // we call a function like this. That means even though we deselect all items above, QGraphicsScene will // continue to incorrectly signal selectionChanged() when items that were selected (but are now not) get // deleted. Calling this function appears to update the internal cache and prevent this. @@ -119,6 +145,21 @@ QList NodeViewScene::GetSelectedItems() const return selected; } +QList NodeViewScene::GetSelectedEdges() const +{ + QList edges; + + QHash::const_iterator i; + + for (i=edge_map_.constBegin(); i!=edge_map_.constEnd(); i++) { + if (i.value()->isSelected()) { + edges.append(i.key()); + } + } + + return edges; +} + const QHash &NodeViewScene::item_map() const { return item_map_; @@ -133,6 +174,7 @@ void NodeViewScene::AddNode(Node* node) { NodeViewItem* item = new NodeViewItem(); + item->SetFlowDirection(direction_); item->SetNode(node); addItem(item); @@ -152,11 +194,15 @@ void NodeViewScene::AddNode(Node* node) } } - QueueReorganize(); + connect(node, &Node::PositionChanged, this, &NodeViewScene::NodePositionChanged); + connect(node, &Node::LabelChanged, this, &NodeViewScene::NodeLabelChanged); } void NodeViewScene::RemoveNode(Node *node) { + disconnect(node, &Node::LabelChanged, this, &NodeViewScene::NodeLabelChanged); + disconnect(node, &Node::PositionChanged, this, &NodeViewScene::NodePositionChanged); + delete item_map_.take(node); } @@ -165,11 +211,10 @@ void NodeViewScene::AddEdge(NodeEdgePtr edge) NodeViewEdge* edge_ui = new NodeViewEdge(); edge_ui->SetEdge(edge); + edge_ui->SetFlowDirection(direction_); addItem(edge_ui); edge_map_.insert(edge.get(), edge_ui); - - QueueReorganize(); } void NodeViewScene::RemoveEdge(NodeEdgePtr edge) @@ -177,146 +222,47 @@ void NodeViewScene::RemoveEdge(NodeEdgePtr edge) delete edge_map_.take(edge.get()); } -void NodeViewScene::QueueReorganize() +Qt::Orientation NodeViewScene::GetFlowOrientation() const { - // Avoids the fairly complex Reorganize() function every single time a connection or node is added - - reorganize_timer_.stop(); - reorganize_timer_.start(20); + return NodeViewCommon::GetFlowOrientation(direction_); } -QList NodeViewScene::GetNodeDirectDescendants(Node* n, const QList connected_nodes, QList& processed_nodes) +NodeViewCommon::FlowDirection NodeViewScene::GetFlowDirection() const { - QList direct_descendants = connected_nodes; - - processed_nodes.append(n); - - // Remove any nodes that aren't necessarily attached directly - for (int i=0;ioutput()->edges().size();j++) { - Node* this_output_connection = connected->output()->edges().at(j)->input()->parentNode(); - if (!processed_nodes.contains(this_output_connection)) { - direct_descendants.removeAt(i); - i--; - break; - } - } - } - - return direct_descendants; + return direction_; } -int NodeViewScene::FindWeightsInternal(Node *node, QHash &weights, QList& weighted_nodes) +void NodeViewScene::ReorganizeFrom(Node* n) { - QList connected_nodes = node->GetImmediateDependencies(); + QList immediates = n->GetImmediateDependencies(); - int weight = 0; - - if (!connected_nodes.isEmpty()) { - QList direct_descendants = GetNodeDirectDescendants(node, connected_nodes, weighted_nodes); - - foreach (Node* dep, direct_descendants) { - weight += FindWeightsInternal(dep, weights, weighted_nodes); - } - } - - weight = qMax(weight, 1); - - weights.insert(node, weight); - - return weight; -} - -void NodeViewScene::ReorganizeInternal(NodeViewItem* src_item, QHash& weights, QList& positioned_nodes) -{ - if (!src_item) { + if (immediates.isEmpty()) { + // Nothing to do return; } - Node* n = src_item->GetNode(); + QPointF parent_pos = n->GetPosition(); - QList connected_nodes = n->GetImmediateDependencies(); + qreal child_x = parent_pos.x() - 1.0; + qreal children_height = immediates.size()-1; + qreal children_y = parent_pos.y() - children_height * 0.5; - if (connected_nodes.isEmpty()) { - return; - } + for (int i=0;iSetPosition(QPointF(child_x, + children_y + i)); - QList direct_descendants = GetNodeDirectDescendants(n, connected_nodes, positioned_nodes); - - int descendant_weight = 0; - foreach (Node* dep, direct_descendants) { - descendant_weight += weights.value(dep); - } - - qreal center_y = src_item->y(); - qreal total_height = descendant_weight * src_item->rect().height() + (direct_descendants.size()-1) * src_item->rect().height()/2; - double item_top = center_y - (total_height/2) + src_item->rect().height()/2; - - // Set each node's position - int weight_index = 0; - for (int i=0;irect().height() * 1.5; - - QPointF item_pos(src_item->pos().x() - item->rect().width() * 3 / 2, - item_y); - - item->setPos(item_pos); - - weight_index += weights.value(connected); - } - - // Recursively work on each node - foreach (Node* connected, connected_nodes) { - NodeViewItem* item = NodeToUIObject(connected); - - if (!item) { - continue; - } - - ReorganizeInternal(item, weights, positioned_nodes); + ReorganizeFrom(immediates.at(i)); } } -void NodeViewScene::Reorganize() +void NodeViewScene::NodePositionChanged(const QPointF &pos) { - if (!graph_) { - return; - } + item_map_.value(static_cast(sender()))->SetNodePosition(pos); +} - QList end_nodes; - - // Calculate the nodes that don't output to anything, they'll be our anchors - foreach (Node* node, graph_->nodes()) { - if (!node->HasConnectedOutputs()) { - end_nodes.append(node); - } - } - - QList processed_nodes; - - QHash node_weights; - foreach (Node* end_node, end_nodes) { - FindWeightsInternal(end_node, node_weights, processed_nodes); - } - - processed_nodes.clear(); - - foreach (Node* end_node, end_nodes) { - ReorganizeInternal(NodeToUIObject(end_node), node_weights, processed_nodes); - } +void NodeViewScene::NodeLabelChanged() +{ + item_map_.value(static_cast(sender()))->update(); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 436045700..d512278e1 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -25,8 +25,8 @@ #include #include "node/graph.h" -#include "widget/nodeview/nodeviewedge.h" -#include "widget/nodeview/nodeviewitem.h" +#include "nodeviewedge.h" +#include "nodeviewitem.h" OLIVE_NAMESPACE_ENTER @@ -65,10 +65,21 @@ public: QList GetSelectedNodes() const; QList GetSelectedItems() const; + QList GetSelectedEdges() const; const QHash& item_map() const; const QHash& edge_map() const; + Qt::Orientation GetFlowOrientation() const; + + NodeViewCommon::FlowDirection GetFlowDirection() const; + void SetFlowDirection(NodeViewCommon::FlowDirection direction); + + /** + * @brief Automatically reposition the nodes based on their connections + */ + void ReorganizeFrom(Node* n); + public slots: /** * @brief Slot when a Node is added to a graph (SetGraph() connects this) @@ -103,27 +114,24 @@ public slots: void RemoveEdge(NodeEdgePtr edge); private: - void QueueReorganize(); - - QList GetNodeDirectDescendants(Node* n, const QList connected_nodes, QList& processed_nodes); - - int FindWeightsInternal(Node* node, QHash& weights, QList& weighted_nodes); - - void ReorganizeInternal(NodeViewItem *src_item, QHash& weights, QList &positioned_nodes); - QHash item_map_; QHash edge_map_; - QTimer reorganize_timer_; - NodeGraph* graph_; + NodeViewCommon::FlowDirection direction_; + private slots: /** - * @brief Automatically reposition the nodes based on their connections + * @brief Receiver for whenever a node position changes */ - void Reorganize(); + void NodePositionChanged(const QPointF& pos); + + /** + * @brief Receiver for when a node's label has changed + */ + void NodeLabelChanged(); }; diff --git a/app/widget/panel/panel.h b/app/widget/panel/panel.h index a0008faf0..1d25f5eba 100644 --- a/app/widget/panel/panel.h +++ b/app/widget/panel/panel.h @@ -165,6 +165,8 @@ public: virtual void ToggleSelectedEnabled(){} + virtual void Duplicate(){} + signals: void CloseRequested(); diff --git a/app/widget/scope/histogram/histogram.cpp b/app/widget/scope/histogram/histogram.cpp index ab7cb66e1..b9028f85a 100644 --- a/app/widget/scope/histogram/histogram.cpp +++ b/app/widget/scope/histogram/histogram.cpp @@ -49,7 +49,9 @@ void HistogramScope::SetBuffer(Frame* frame) { buffer_ = frame; - StartUpdate(); + if (isVisible()) { + StartUpdate(); + } } void HistogramScope::FinishedProcessing(QVector red, QVector green, QVector blue) @@ -111,6 +113,13 @@ void HistogramScope::StartUpdate() } } +void HistogramScope::showEvent(QShowEvent* e) +{ + ManagedDisplayWidget::showEvent(e); + + StartUpdate(); +} + HistogramScopeWorker::HistogramScopeWorker() : cancelled_(false) { diff --git a/app/widget/scope/histogram/histogram.h b/app/widget/scope/histogram/histogram.h index ea5175726..3efad3919 100644 --- a/app/widget/scope/histogram/histogram.h +++ b/app/widget/scope/histogram/histogram.h @@ -78,6 +78,8 @@ protected: virtual void ColorProcessorChangedEvent() override; + virtual void showEvent(QShowEvent* e) override; + private: void StartUpdate(); diff --git a/app/widget/scope/waveform/waveform.cpp b/app/widget/scope/waveform/waveform.cpp index 8a30dac66..211e66ef3 100644 --- a/app/widget/scope/waveform/waveform.cpp +++ b/app/widget/scope/waveform/waveform.cpp @@ -21,6 +21,11 @@ #include "waveform.h" +#include +#include +#include + +#include "common/qtutils.h" #include "node/node.h" #include "render/backend/opengl/openglrenderfunctions.h" @@ -28,16 +33,32 @@ OLIVE_NAMESPACE_ENTER WaveformScope::WaveformScope(QWidget* parent) : ManagedDisplayWidget(parent), - texture_(nullptr) + buffer_(nullptr) { EnableDefaultContextMenu(); } -void WaveformScope::SetTexture(OpenGLTexture *texture) +WaveformScope::~WaveformScope() { - texture_ = texture; + CleanUp(); - update(); + if (context()) { + disconnect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &WaveformScope::CleanUp); + } +} + +void WaveformScope::SetBuffer(Frame *frame) +{ + buffer_ = frame; + + UploadTextureFromBuffer(); +} + +void WaveformScope::showEvent(QShowEvent* e) +{ + ManagedDisplayWidget::showEvent(e); + + UploadTextureFromBuffer(); } void WaveformScope::initializeGL() @@ -49,37 +70,153 @@ void WaveformScope::initializeGL() pipeline_->addShaderFromSourceCode(QOpenGLShader::Vertex, OpenGLShader::CodeDefaultVertex()); pipeline_->addShaderFromSourceCode(QOpenGLShader::Fragment, Node::ReadFileAsString(":/shaders/rgbwaveform.frag")); pipeline_->link(); + + framebuffer_.Create(context()); + + connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &WaveformScope::CleanUp, Qt::DirectConnection); + + UploadTextureFromBuffer(); } void WaveformScope::paintGL() { - context()->functions()->glClearColor(0, 0, 0, 0); - context()->functions()->glClear(GL_COLOR_BUFFER_BIT); + QOpenGLFunctions* f = context()->functions(); - if (!pipeline_ || !texture_) { + f->glClearColor(0, 0, 0, 0); + f->glClear(GL_COLOR_BUFFER_BIT); + + float waveform_scale = 0.80f; + float waveform_dim_x = width() * waveform_scale; + float waveform_dim_y = height() * waveform_scale; + float waveform_start_dim_x = (width() - waveform_dim_x) / 2.0f; + float waveform_start_dim_y = (height() - waveform_dim_y) / 2.0f; + float waveform_end_dim_x = width() - waveform_start_dim_x; + float waveform_end_dim_y = height() - waveform_start_dim_y; + + if (buffer_ && pipeline_ && texture_.IsCreated()) { + // Convert reference frame to display space + framebuffer_.Attach(&managed_tex_); + framebuffer_.Bind(); + + texture_.Bind(); + + f->glViewport(0, 0, texture_.width(), texture_.height()); + + color_service()->ProcessOpenGL(); + + texture_.Release(); + + framebuffer_.Release(); + framebuffer_.Detach(); + + // Draw waveform through shader + pipeline_->bind(); + pipeline_->setUniformValue("ove_resolution", texture_.width(), texture_.height()); + pipeline_->setUniformValue("ove_viewport", width(), height()); + GLfloat luma[3] = {0.0, 0.0, 0.0}; + color_manager()->GetDefaultLumaCoefs(luma); + pipeline_->setUniformValue("luma_coeffs", luma[0], luma[1], luma[2]); + + // Scale of the waveform relative to the viewport surface. + pipeline_->setUniformValue("waveform_scale", waveform_scale); + pipeline_->setUniformValue( + "waveform_dims", waveform_dim_x, waveform_dim_y); + + pipeline_->setUniformValue( + "waveform_region", + waveform_start_dim_x, waveform_start_dim_y, + waveform_end_dim_x, waveform_end_dim_y); + + float waveform_start_uv_x = waveform_start_dim_x / width(); + float waveform_start_uv_y = waveform_start_dim_y / height(); + float waveform_end_uv_x = waveform_end_dim_x / width(); + float waveform_end_uv_y = waveform_end_dim_y / height(); + pipeline_->setUniformValue( + "waveform_uv", + waveform_start_uv_x, waveform_start_uv_y, + waveform_end_uv_x, waveform_end_uv_y); + + pipeline_->release(); + + f->glViewport(0, 0, width(), height()); + + managed_tex_.Bind(); + + OpenGLRenderFunctions::Blit(pipeline_); + + managed_tex_.Release(); + } + + // Draw line overlays + QPainter p(this); + QFontMetrics font_metrics = QFontMetrics(QFont()); + QString label; + float ire_increment = 0.1f; + float ire_steps = int(1.0 / ire_increment); + QVector ire_lines(ire_steps + 1); + int font_x_offset = 0; + int font_y_offset = font_metrics.capHeight() / 2.0f; + + p.setCompositionMode(QPainter::CompositionMode_Plus); + + p.setPen(QColor(0.0, 0.6 * 255.0, 0.0)); + p.setFont(QFont()); + + for (int i=0; i <= ire_steps; i++) { + ire_lines[i].setLine( + waveform_start_dim_x, + (waveform_dim_y * (i * ire_increment)) + waveform_start_dim_y, + waveform_end_dim_x, + (waveform_dim_y * (i * ire_increment)) + waveform_start_dim_y); + label = QString::number(1.0 - (i * ire_increment), 'f', 1); + font_x_offset = QFontMetricsWidth(font_metrics, label) + 4; + + p.drawText( + waveform_start_dim_x - font_x_offset, + (waveform_dim_y * (i * ire_increment)) + waveform_start_dim_y + font_y_offset, + label); + } + p.drawLines(ire_lines); +} + +void WaveformScope::UploadTextureFromBuffer() +{ + if (!isVisible()) { return; } - pipeline_->bind(); - pipeline_->setUniformValue("ove_resolution", texture_->width(), texture_->height()); - pipeline_->setUniformValue("ove_viewport", width(), height()); + if (buffer_) { + makeCurrent(); - // The general size of a pixel - pipeline_->setUniformValue("threshold", 2.0f / static_cast(height())); + if (!texture_.IsCreated() + || texture_.width() != buffer_->width() + || texture_.height() != buffer_->height() + || texture_.format() != buffer_->format()) { + texture_.Destroy(); + managed_tex_.Destroy(); - pipeline_->release(); + texture_.Create(context(), buffer_); + managed_tex_.Create(context(), buffer_->width(), buffer_->height(), buffer_->format()); + } else { + texture_.Upload(buffer_); + } - texture_->Bind(); + doneCurrent(); + } - OpenGLRenderFunctions::Blit(pipeline_); - - texture_->Release(); + update(); } void WaveformScope::CleanUp() { + makeCurrent(); + pipeline_ = nullptr; - texture_ = nullptr; + texture_.Destroy(); + managed_tex_.Destroy(); + framebuffer_.Destroy(); + + doneCurrent(); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/scope/waveform/waveform.h b/app/widget/scope/waveform/waveform.h index f9f930c5c..4243a9d3a 100644 --- a/app/widget/scope/waveform/waveform.h +++ b/app/widget/scope/waveform/waveform.h @@ -23,6 +23,7 @@ #include "codec/frame.h" #include "render/backend/opengl/openglcolorprocessor.h" +#include "render/backend/opengl/openglframebuffer.h" #include "render/backend/opengl/openglshader.h" #include "render/backend/opengl/opengltexture.h" #include "widget/manageddisplay/manageddisplay.h" @@ -35,18 +36,30 @@ class WaveformScope : public ManagedDisplayWidget public: WaveformScope(QWidget* parent = nullptr); + virtual ~WaveformScope() override; + public slots: - void SetTexture(OpenGLTexture* texture); + void SetBuffer(Frame* frame); protected: virtual void initializeGL() override; virtual void paintGL() override; + virtual void showEvent(QShowEvent* e) override; + private: + void UploadTextureFromBuffer(); + OpenGLShaderPtr pipeline_; - OpenGLTexture* texture_; + OpenGLTexture texture_; + + OpenGLTexture managed_tex_; + + OpenGLFramebuffer framebuffer_; + + Frame* buffer_; private slots: void CleanUp(); diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 848b15585..31d17bb65 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -91,7 +91,7 @@ void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) if (!(event->GetModifiers() & Qt::AltModifier)) { parent()->SetBlockLinksSelected(item->block(), true); } - } else { + } else if (event->GetButton() == Qt::LeftButton) { // Start rubberband drag parent()->StartRubberBandSelect(true, !(event->GetModifiers() & Qt::AltModifier)); diff --git a/app/widget/timelinewidget/view/CMakeLists.txt b/app/widget/timelinewidget/view/CMakeLists.txt index 5bbce0175..c2da0eead 100644 --- a/app/widget/timelinewidget/view/CMakeLists.txt +++ b/app/widget/timelinewidget/view/CMakeLists.txt @@ -16,6 +16,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + widget/timelinewidget/view/handmovableview.h + widget/timelinewidget/view/handmovableview.cpp widget/timelinewidget/view/timelineplayhead.h widget/timelinewidget/view/timelineplayhead.cpp widget/timelinewidget/view/timelineview.h diff --git a/app/widget/timelinewidget/view/handmovableview.cpp b/app/widget/timelinewidget/view/handmovableview.cpp new file mode 100644 index 000000000..f0995eb7b --- /dev/null +++ b/app/widget/timelinewidget/view/handmovableview.cpp @@ -0,0 +1,118 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "handmovableview.h" + +#include + +#include "core.h" + +OLIVE_NAMESPACE_ENTER + +HandMovableView::HandMovableView(QWidget* parent) : + QGraphicsView(parent), + dragging_hand_(false) +{ + connect(Core::instance(), &Core::ToolChanged, this, &HandMovableView::ApplicationToolChanged); +} + +void HandMovableView::ApplicationToolChanged(Tool::Item tool) +{ + if (tool == Tool::kHand) { + setDragMode(ScrollHandDrag); + } else { + setDragMode(default_drag_mode_); + } + + ToolChangedEvent(tool); +} + +bool HandMovableView::HandPress(QMouseEvent *event) +{ + if (event->button() == Qt::MiddleButton) { + pre_hand_drag_mode_ = dragMode(); + dragging_hand_ = true; + + setDragMode(ScrollHandDrag); + + // Transform mouse event to act like the left button is pressed + QMouseEvent transformed(event->type(), + event->localPos(), + Qt::LeftButton, + Qt::LeftButton, + event->modifiers()); + + QGraphicsView::mousePressEvent(&transformed); + + return true; + } + + return false; +} + +bool HandMovableView::HandMove(QMouseEvent *event) +{ + if (dragging_hand_) { + // Transform mouse event to act like the left button is pressed + QMouseEvent transformed(event->type(), + event->localPos(), + Qt::LeftButton, + Qt::LeftButton, + event->modifiers()); + + QGraphicsView::mouseMoveEvent(&transformed); + } + return dragging_hand_; +} + +bool HandMovableView::HandRelease(QMouseEvent *event) +{ + if (dragging_hand_) { + // Transform mouse event to act like the left button is pressed + QMouseEvent transformed(event->type(), + event->localPos(), + Qt::LeftButton, + Qt::LeftButton, + event->modifiers()); + + QGraphicsView::mouseReleaseEvent(&transformed); + + setDragMode(pre_hand_drag_mode_); + + dragging_hand_ = false; + + return true; + } + + return false; +} + +void HandMovableView::SetDefaultDragMode(QGraphicsView::DragMode mode) +{ + default_drag_mode_ = mode; + setDragMode(default_drag_mode_); +} + +const QGraphicsView::DragMode &HandMovableView::GetDefaultDragMode() const +{ + return default_drag_mode_; +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/view/handmovableview.h b/app/widget/timelinewidget/view/handmovableview.h new file mode 100644 index 000000000..9224069c7 --- /dev/null +++ b/app/widget/timelinewidget/view/handmovableview.h @@ -0,0 +1,59 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef HANDMOVABLEVIEW_H +#define HANDMOVABLEVIEW_H + +#include + +#include "tool/tool.h" + +OLIVE_NAMESPACE_ENTER + +class HandMovableView : public QGraphicsView +{ + Q_OBJECT +public: + HandMovableView(QWidget* parent = nullptr); + +protected: + virtual void ToolChangedEvent(Tool::Item tool){Q_UNUSED(tool)} + + bool HandPress(QMouseEvent* event); + bool HandMove(QMouseEvent* event); + bool HandRelease(QMouseEvent* event); + + void SetDefaultDragMode(DragMode mode); + const DragMode& GetDefaultDragMode() const; + +private: + bool dragging_hand_; + DragMode pre_hand_drag_mode_; + + DragMode default_drag_mode_; + +private slots: + void ApplicationToolChanged(Tool::Item tool); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // HANDMOVABLEVIEW_H diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index 04dc87864..bcf13b5b7 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -81,7 +81,7 @@ void TimelineView::mousePressEvent(QMouseEvent *event) return; } - TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), event->modifiers()); + TimelineViewMouseEvent timeline_event = CreateMouseEvent(event); emit MousePressed(&timeline_event); } @@ -98,7 +98,7 @@ void TimelineView::mouseMoveEvent(QMouseEvent *event) return; } - TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), event->modifiers()); + TimelineViewMouseEvent timeline_event = CreateMouseEvent(event); emit MouseMoved(&timeline_event); } @@ -115,14 +115,14 @@ void TimelineView::mouseReleaseEvent(QMouseEvent *event) return; } - TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), event->modifiers()); + TimelineViewMouseEvent timeline_event = CreateMouseEvent(event); emit MouseReleased(&timeline_event); } void TimelineView::mouseDoubleClickEvent(QMouseEvent *event) { - TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), event->modifiers()); + TimelineViewMouseEvent timeline_event = CreateMouseEvent(event); emit MouseDoubleClicked(&timeline_event); } @@ -146,7 +146,7 @@ void TimelineView::wheelEvent(QWheelEvent *event) void TimelineView::dragEnterEvent(QDragEnterEvent *event) { - TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), event->keyboardModifiers()); + TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), Qt::NoButton, event->keyboardModifiers()); timeline_event.SetMimeData(event->mimeData()); timeline_event.SetEvent(event); @@ -156,7 +156,7 @@ void TimelineView::dragEnterEvent(QDragEnterEvent *event) void TimelineView::dragMoveEvent(QDragMoveEvent *event) { - TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), event->keyboardModifiers()); + TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), Qt::NoButton, event->keyboardModifiers()); timeline_event.SetMimeData(event->mimeData()); timeline_event.SetEvent(event); @@ -171,7 +171,7 @@ void TimelineView::dragLeaveEvent(QDragLeaveEvent *event) void TimelineView::dropEvent(QDropEvent *event) { - TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), event->keyboardModifiers()); + TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), Qt::NoButton, event->keyboardModifiers()); timeline_event.SetMimeData(event->mimeData()); timeline_event.SetEvent(event); @@ -273,17 +273,21 @@ TimelineCoordinate TimelineView::SceneToCoordinate(const QPointF& pt) return TimelineCoordinate(SceneToTime(pt.x()), TrackReference(ConnectedTrackType(), SceneToTrack(pt.y()))); } -TimelineViewMouseEvent TimelineView::CreateMouseEvent(const QPoint& pos, Qt::KeyboardModifiers modifiers) +TimelineViewMouseEvent TimelineView::CreateMouseEvent(QMouseEvent *event) +{ + return CreateMouseEvent(event->pos(), event->button(), event->modifiers()); +} + +TimelineViewMouseEvent TimelineView::CreateMouseEvent(const QPoint& pos, Qt::MouseButton button, Qt::KeyboardModifiers modifiers) { QPointF scene_pt = mapToScene(pos); - TimelineViewMouseEvent timeline_event(scene_pt.x(), - GetScale(), - timebase(), - TrackReference(ConnectedTrackType(), SceneToTrack(scene_pt.y())), - modifiers); - - return timeline_event; + return TimelineViewMouseEvent(scene_pt.x(), + GetScale(), + timebase(), + TrackReference(ConnectedTrackType(), SceneToTrack(scene_pt.y())), + button, + modifiers); } int TimelineView::GetHeightOfAllTracks() const diff --git a/app/widget/timelinewidget/view/timelineview.h b/app/widget/timelinewidget/view/timelineview.h index ecf66c2b7..53daceca0 100644 --- a/app/widget/timelinewidget/view/timelineview.h +++ b/app/widget/timelinewidget/view/timelineview.h @@ -100,7 +100,8 @@ private: TimelineCoordinate ScreenToCoordinate(const QPoint& pt); TimelineCoordinate SceneToCoordinate(const QPointF& pt); - TimelineViewMouseEvent CreateMouseEvent(const QPoint &pos, Qt::KeyboardModifiers modifiers); + TimelineViewMouseEvent CreateMouseEvent(QMouseEvent* event); + TimelineViewMouseEvent CreateMouseEvent(const QPoint &pos, Qt::MouseButton button, Qt::KeyboardModifiers modifiers); int GetHeightOfAllTracks() const; diff --git a/app/widget/timelinewidget/view/timelineviewbase.cpp b/app/widget/timelinewidget/view/timelineviewbase.cpp index 31f53b586..9970e7115 100644 --- a/app/widget/timelinewidget/view/timelineviewbase.cpp +++ b/app/widget/timelinewidget/view/timelineviewbase.cpp @@ -34,12 +34,11 @@ OLIVE_NAMESPACE_ENTER const double TimelineViewBase::kMaximumScale = 8192; TimelineViewBase::TimelineViewBase(QWidget *parent) : - QGraphicsView(parent), + HandMovableView(parent), playhead_(0), playhead_scene_left_(-1), playhead_scene_right_(-1), dragging_playhead_(false), - dragging_hand_(false), limit_y_axis_(false) { setScene(&scene_); @@ -50,7 +49,6 @@ TimelineViewBase::TimelineViewBase(QWidget *parent) : SetDefaultDragMode(NoDrag); connect(&scene_, SIGNAL(changed(const QList&)), this, SLOT(UpdateSceneRect())); - connect(Core::instance(), &Core::ToolChanged, this, &TimelineViewBase::ApplicationToolChanged); SetMaximumScale(kMaximumScale); } @@ -100,17 +98,6 @@ rational TimelineViewBase::GetPlayheadTime() const return Timecode::timestamp_to_time(playhead_, timebase()); } -void TimelineViewBase::SetDefaultDragMode(QGraphicsView::DragMode mode) -{ - default_drag_mode_ = mode; - setDragMode(default_drag_mode_); -} - -const QGraphicsView::DragMode &TimelineViewBase::GetDefaultDragMode() const -{ - return default_drag_mode_; -} - bool TimelineViewBase::PlayheadPress(QMouseEvent *event) { QPointF scene_pos = mapToScene(event->pos()); @@ -148,70 +135,6 @@ bool TimelineViewBase::PlayheadRelease(QMouseEvent*) return false; } -bool TimelineViewBase::HandPress(QMouseEvent *event) -{ - if (event->button() == Qt::MiddleButton) { - pre_hand_drag_mode_ = dragMode(); - dragging_hand_ = true; - - setDragMode(ScrollHandDrag); - - // Transform mouse event to act like the left button is pressed - QMouseEvent transformed(event->type(), - event->localPos(), - Qt::LeftButton, - Qt::LeftButton, - event->modifiers()); - - QGraphicsView::mousePressEvent(&transformed); - - return true; - } - - return false; -} - -bool TimelineViewBase::HandMove(QMouseEvent *event) -{ - if (dragging_hand_) { - // Transform mouse event to act like the left button is pressed - QMouseEvent transformed(event->type(), - event->localPos(), - Qt::LeftButton, - Qt::LeftButton, - event->modifiers()); - - QGraphicsView::mouseMoveEvent(&transformed); - } - return dragging_hand_; -} - -bool TimelineViewBase::HandRelease(QMouseEvent *event) -{ - if (dragging_hand_) { - // Transform mouse event to act like the left button is pressed - QMouseEvent transformed(event->type(), - event->localPos(), - Qt::LeftButton, - Qt::LeftButton, - event->modifiers()); - - QGraphicsView::mouseReleaseEvent(&transformed); - - setDragMode(pre_hand_drag_mode_); - - dragging_hand_ = false; - - return true; - } - - return false; -} - -void TimelineViewBase::ToolChangedEvent(Tool::Item) -{ -} - qreal TimelineViewBase::GetPlayheadX() { return TimeToScene(Timecode::timestamp_to_time(playhead_, timebase())); @@ -305,15 +228,4 @@ void TimelineViewBase::SetLimitYAxis(bool) UpdateSceneRect(); } -void TimelineViewBase::ApplicationToolChanged(Tool::Item tool) -{ - if (tool == Tool::kHand) { - setDragMode(ScrollHandDrag); - } else { - setDragMode(default_drag_mode_); - } - - ToolChangedEvent(tool); -} - OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/view/timelineviewbase.h b/app/widget/timelinewidget/view/timelineviewbase.h index c64718bb6..55eb9ff62 100644 --- a/app/widget/timelinewidget/view/timelineviewbase.h +++ b/app/widget/timelinewidget/view/timelineviewbase.h @@ -24,12 +24,13 @@ #include #include "core.h" +#include "handmovableview.h" #include "timelineplayhead.h" #include "widget/timelinewidget/timelinescaledobject.h" OLIVE_NAMESPACE_ENTER -class TimelineViewBase : public QGraphicsView, public TimelineScaledObject +class TimelineViewBase : public HandMovableView, public TimelineScaledObject { Q_OBJECT public: @@ -66,19 +67,10 @@ protected: rational GetPlayheadTime() const; - void SetDefaultDragMode(DragMode mode); - const DragMode& GetDefaultDragMode() const; - bool PlayheadPress(QMouseEvent* event); bool PlayheadMove(QMouseEvent* event); bool PlayheadRelease(QMouseEvent* event); - bool HandPress(QMouseEvent* event); - bool HandMove(QMouseEvent* event); - bool HandRelease(QMouseEvent* event); - - virtual void ToolChangedEvent(Tool::Item tool); - virtual void TimebaseChangedEvent(const rational &) override; private: @@ -93,15 +85,10 @@ private: bool dragging_playhead_; - bool dragging_hand_; - DragMode pre_hand_drag_mode_; - QGraphicsScene scene_; bool limit_y_axis_; - DragMode default_drag_mode_; - rational end_time_; private slots: @@ -118,8 +105,6 @@ private slots: */ void PageScrollToPlayhead(); - void ApplicationToolChanged(Tool::Item tool); - }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/view/timelineviewblockitem.cpp b/app/widget/timelinewidget/view/timelineviewblockitem.cpp index 7e182657f..81edaa2ed 100644 --- a/app/widget/timelinewidget/view/timelineviewblockitem.cpp +++ b/app/widget/timelinewidget/view/timelineviewblockitem.cpp @@ -145,7 +145,7 @@ void TimelineViewBlockItem::paint(QPainter *painter, const QStyleOptionGraphicsI QFontMetrics fm = painter->fontMetrics(); int text_width = qMin(qRound(rect().width()), QFontMetricsWidth(fm, block_->block_name())); - QPointF underline_start = rect().topLeft() + QPointF(0, fm.height()); + QPointF underline_start = rect().topLeft() + QPointF(0, text_top + fm.height()); QPointF underline_end = underline_start + QPointF(text_width, 0); painter->drawLine(underline_start, underline_end); diff --git a/app/widget/timelinewidget/view/timelineviewmouseevent.cpp b/app/widget/timelinewidget/view/timelineviewmouseevent.cpp index fa8baf8df..4e4dc183c 100644 --- a/app/widget/timelinewidget/view/timelineviewmouseevent.cpp +++ b/app/widget/timelinewidget/view/timelineviewmouseevent.cpp @@ -30,11 +30,13 @@ TimelineViewMouseEvent::TimelineViewMouseEvent(const qreal &scene_x, const double &scale_x, const rational &timebase, const TrackReference &track, + const Qt::MouseButton &button, const Qt::KeyboardModifiers &modifiers) : scene_x_(scene_x), scale_x_(scale_x), timebase_(timebase), track_(track), + button_(button), modifiers_(modifiers), source_event_(nullptr), mime_data_(nullptr) @@ -81,6 +83,11 @@ const qreal &TimelineViewMouseEvent::GetSceneX() const return scene_x_; } +const Qt::MouseButton &TimelineViewMouseEvent::GetButton() const +{ + return button_; +} + void TimelineViewMouseEvent::accept() { if (source_event_ != nullptr) diff --git a/app/widget/timelinewidget/view/timelineviewmouseevent.h b/app/widget/timelinewidget/view/timelineviewmouseevent.h index 65264078c..b99db096f 100644 --- a/app/widget/timelinewidget/view/timelineviewmouseevent.h +++ b/app/widget/timelinewidget/view/timelineviewmouseevent.h @@ -36,6 +36,7 @@ public: const double& scale_x, const rational& timebase, const TrackReference &track, + const Qt::MouseButton &button, const Qt::KeyboardModifiers& modifiers = Qt::NoModifier); TimelineCoordinate GetCoordinates(bool round_time = false) const; @@ -61,6 +62,8 @@ public: const qreal& GetSceneX() const; + const Qt::MouseButton& GetButton() const; + void accept(); void ignore(); @@ -71,6 +74,8 @@ private: TrackReference track_; + Qt::MouseButton button_; + Qt::KeyboardModifiers modifiers_; QEvent* source_event_; diff --git a/app/widget/viewer/manageddisplayobject.cpp b/app/widget/viewer/manageddisplayobject.cpp deleted file mode 100644 index f7541ca3f..000000000 --- a/app/widget/viewer/manageddisplayobject.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "manageddisplayobject.h" - -ManagedDisplayObject::ManagedDisplayObject() -{ - -} diff --git a/app/widget/viewer/manageddisplayobject.h b/app/widget/viewer/manageddisplayobject.h deleted file mode 100644 index 11de19c12..000000000 --- a/app/widget/viewer/manageddisplayobject.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef MANAGEDDISPLAYOBJECT_H -#define MANAGEDDISPLAYOBJECT_H - - -class ManagedDisplayObject -{ -public: - ManagedDisplayObject(); -}; - -#endif // MANAGEDDISPLAYOBJECT_H diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 4fd344bb9..6d5ae3976 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -66,8 +66,6 @@ ViewerWidget::ViewerWidget(QWidget *parent) : connect(main_widget, &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); connect(main_widget, &ViewerDisplayWidget::CursorColor, this, &ViewerWidget::CursorColor); connect(main_widget, &ViewerDisplayWidget::LoadedBuffer, this, &ViewerWidget::LoadedBuffer); - connect(main_widget, &ViewerDisplayWidget::LoadedTexture, this, &ViewerWidget::LoadedTexture); - connect(main_widget, &ViewerDisplayWidget::DrewManagedTexture, this, &ViewerWidget::DrewManagedTexture); connect(main_widget, &ViewerDisplayWidget::ColorProcessorChanged, this, &ViewerWidget::ColorProcessorChanged); connect(main_widget, &ViewerDisplayWidget::ColorManagerChanged, this, &ViewerWidget::ColorManagerChanged); connect(sizer_, &ViewerSizer::RequestMatrix, main_widget, &ViewerDisplayWidget::SetMatrix); @@ -750,11 +748,6 @@ void ViewerWidget::SetSignalCursorColorEnabled(bool e) } } -void ViewerWidget::SetEmitDrewManagedTextureEnabled(bool e) -{ - main_gl_widget()->SetEmitDrewManagedTextureEnabled(e); -} - void ViewerWidget::TimebaseChangedEvent(const rational &timebase) { TimeBasedWidget::TimebaseChangedEvent(timebase); diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 694e74100..6d22e89a8 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -109,11 +109,6 @@ public slots: */ void SetSignalCursorColorEnabled(bool e); - /** - * @brief Wrapper for ViewerGLWidget::SetEmitDrewManagedTextureEnabled() - */ - void SetEmitDrewManagedTextureEnabled(bool e); - signals: /** * @brief Wrapper for ViewerGLWidget::CursorColor() @@ -125,16 +120,6 @@ signals: */ void LoadedBuffer(Frame* load_buffer); - /** - * @brief Wrapper for ViewerGLWidget::LoadedTexture() - */ - void LoadedTexture(OpenGLTexture* texture); - - /** - * @brief Wrapper for ViewerGLWidget::DrewManagedTexture() - */ - void DrewManagedTexture(OpenGLTexture* texture); - /** * @brief Request a scope panel * diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 9d0531726..b5226f9c0 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -42,10 +42,8 @@ bool ViewerDisplayWidget::nouveau_check_done_ = false; ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) : ManagedDisplayWidget(parent), - managed_copy_pipeline_(nullptr), has_image_(false), signal_cursor_color_(false), - enable_display_referred_signal_(false), gizmos_(nullptr) { } @@ -92,14 +90,12 @@ void ViewerDisplayWidget::SetImage(const QString &fn) input->read_image(input->spec().format, load_buffer_.data(), OIIO::AutoStride, load_buffer_.linesize_bytes()); input->close(); - emit LoadedBuffer(&load_buffer_); - - texture_.Upload(load_buffer_.data(), load_buffer_.linesize_pixels()); - - emit LoadedTexture(&texture_); + texture_.Upload(&load_buffer_); doneCurrent(); + emit LoadedBuffer(&load_buffer_); + has_image_ = true; #if OIIO_VERSION < 10903 @@ -139,7 +135,7 @@ void ViewerDisplayWidget::SetImageFromLoadBuffer(Frame *in_buffer) || texture_.format() != in_buffer->format()) { texture_.Create(context(), in_buffer->width(), in_buffer->height(), in_buffer->format(), in_buffer->data(), load_buffer_.linesize_pixels()); } else { - texture_.Upload(in_buffer->data(), load_buffer_.linesize_pixels()); + texture_.Upload(in_buffer); } doneCurrent(); @@ -148,18 +144,6 @@ void ViewerDisplayWidget::SetImageFromLoadBuffer(Frame *in_buffer) update(); } -void ViewerDisplayWidget::SetEmitDrewManagedTextureEnabled(bool e) -{ - enable_display_referred_signal_ = e; - - if (!enable_display_referred_signal_) { - // Destroy the texture now - managed_texture_.Destroy(); - managed_copy_pipeline_ = nullptr; - framebuffer_.Destroy(); - } -} - void ViewerDisplayWidget::ConnectSibling(ViewerDisplayWidget *sibling) { connect(this, &ViewerDisplayWidget::LoadedBuffer, sibling, &ViewerDisplayWidget::SetImageFromLoadBuffer, Qt::QueuedConnection); @@ -251,33 +235,6 @@ void ViewerDisplayWidget::paintGL() // We only draw if we have a pipeline if (has_image_ && color_service() && texture_.IsCreated()) { - // If we're distributing our display-referred final buffer, we'll have to make a copy of it - if (enable_display_referred_signal_) { - - if (!managed_texture_.IsCreated() - || managed_texture_.width() != texture_.width() - || managed_texture_.height() != texture_.height() - || managed_texture_.format() != texture_.format()) { - managed_texture_.Destroy(); - - managed_texture_.Create(context(), texture_.width(), texture_.height(), texture_.format()); - } - - if (!managed_copy_pipeline_) { - managed_copy_pipeline_ = OpenGLShader::CreateDefault(); - } - - if (!framebuffer_.IsCreated()) { - framebuffer_.Create(context()); - } - - framebuffer_.Attach(&managed_texture_); - framebuffer_.Bind(); - - f->glViewport(0, 0, managed_texture_.width(), managed_texture_.height()); - - } - // Bind retrieved texture f->glBindTexture(GL_TEXTURE_2D, texture_.texture()); @@ -287,24 +244,6 @@ void ViewerDisplayWidget::paintGL() // Release retrieved texture f->glBindTexture(GL_TEXTURE_2D, 0); - if (enable_display_referred_signal_) { - - framebuffer_.Release(); - framebuffer_.Detach(); - - emit DrewManagedTexture(&managed_texture_); - - // Bind retrieved texture - managed_texture_.Bind(); - - f->glViewport(0, 0, width(), height()); - - OpenGLRenderFunctions::Blit(managed_copy_pipeline_); - - // Bind retrieved texture - managed_texture_.Release(); - - } } // Draw gizmos if we have any @@ -362,10 +301,7 @@ void ViewerDisplayWidget::ContextCleanup() { makeCurrent(); - managed_copy_pipeline_ = nullptr; texture_.Destroy(); - managed_texture_.Destroy(); - framebuffer_.Destroy(); doneCurrent(); } diff --git a/app/widget/viewer/viewerdisplay.h b/app/widget/viewer/viewerdisplay.h index f35338631..4d3abb1da 100644 --- a/app/widget/viewer/viewerdisplay.h +++ b/app/widget/viewer/viewerdisplay.h @@ -104,15 +104,6 @@ public slots: */ void SetImageFromLoadBuffer(Frame* in_buffer); - /** - * @brief Enables or disables DrewManagedTexture() - * - * To emit a display referred texture, it needs to be copied after the color transform is complete. This naturally - * adds extra GPU cycles that are wasted if there's nothing receiving the signal. Therefore, the signal is disabled - * by default. - */ - void SetEmitDrewManagedTextureEnabled(bool e); - signals: /** * @brief Signal emitted when the user starts dragging from the viewer @@ -133,18 +124,6 @@ signals: */ void LoadedBuffer(Frame* load_buffer); - /** - * @brief Signal emitted when a buffer is loaded into a texture - * - * This texture will be the direct output of the renderer in reference space in GPU VRAM. - */ - void LoadedTexture(OpenGLTexture* texture); - - /** - * @brief Emitted when the a texture has been transformed to display - */ - void DrewManagedTexture(OpenGLTexture* texture); - protected: /** * @brief Override the mouse press event simply to emit the DragStarted() signal @@ -176,23 +155,6 @@ private: */ OpenGLTexture texture_; - /** - * @brief Internal framebuffer used to draw to managed_texture_ - */ - OpenGLFramebuffer framebuffer_; - - /** - * @brief Internal referenceto the OpenGL texture that's been managed - * - * Kept so that scopes can use the display-referred buffer without having to transform again. - */ - OpenGLTexture managed_texture_; - - /** - * @brief Pipeline used to draw to managed_texture_ - */ - OpenGLShaderPtr managed_copy_pipeline_; - /** * @brief Drawing matrix (defaults to identity) */ @@ -213,8 +175,6 @@ private: ViewerSafeMarginInfo safe_margin_; - bool enable_display_referred_signal_; - Node* gizmos_; private slots: diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index e35b83531..46e7527f5 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -83,7 +83,7 @@ MainMenu::MainMenu(MainWindow *parent) : edit_menu_->addAction(edit_redo_item_); edit_menu_->addSeparator(); - MenuShared::instance()->AddItemsForEditMenu(edit_menu_); + MenuShared::instance()->AddItemsForEditMenu(edit_menu_, true); edit_menu_->addSeparator(); edit_select_all_item_ = edit_menu_->AddItem("selectall", this, &MainMenu::SelectAllTriggered, "Ctrl+A"); edit_deselect_all_item_ = edit_menu_->AddItem("deselectall", this, &MainMenu::DeselectAllTriggered, "Ctrl+Shift+A"); @@ -186,7 +186,6 @@ MainMenu::MainMenu(MainWindow *parent) : // WINDOW MENU // window_menu_ = new Menu(this, this, &MainMenu::WindowMenuAboutToShow); - connect(window_menu_, &Menu::aboutToHide, this, &MainMenu::WindowMenuAboutToHide); window_menu_separator_ = window_menu_->addSeparator(); window_maximize_panel_item_ = window_menu_->AddItem("maximizepanel", parent, &MainWindow::ToggleMaximizedPanel, "`"); window_lock_layout_item_ = window_menu_->AddItem("lockpanels", PanelManager::instance(), &PanelManager::SetPanelsLocked); @@ -365,27 +364,37 @@ void MainMenu::PlaybackMenuAboutToShow() void MainMenu::WindowMenuAboutToShow() { - // QMainWindow generates a perfectly usable menu for this purpose, we just need to copy it to the window menu - QMenu* panel_menu = static_cast(parentWidget())->createPopupMenu(); - QList panel_menu_actions = panel_menu->actions(); - - // Make sure when we delete the panel_menu, it doesn't delete the actions - foreach (QAction* panel_action, panel_menu_actions) { - panel_action->setParent(window_menu_); - } - - delete panel_menu; - - window_menu_->insertActions(window_menu_separator_, panel_menu_actions); - - window_lock_layout_item_->setChecked(PanelManager::instance()->ArePanelsLocked()); -} - -void MainMenu::WindowMenuAboutToHide() -{ + // Remove any previous items while (window_menu_->actions().first() != window_menu_separator_) { window_menu_->removeAction(window_menu_->actions().first()); } + + QList panel_actions; + + // Alphabetize actions - keeps actions in a consistent order since PanelManager::panels() is + // ordered from most recently focused to least, which may be confusing user experience. + foreach (PanelWidget* panel, PanelManager::instance()->panels()) { + QAction* panel_action = panel->toggleViewAction(); + + bool inserted = false; + + for (int i=0;itext() > panel_action->text()) { + panel_actions.insert(i, panel_action); + inserted = true; + break; + } + } + + if (!inserted) { + panel_actions.append(panel_action); + } + } + + // Add new items + window_menu_->insertActions(window_menu_separator_, panel_actions); + + window_lock_layout_item_->setChecked(PanelManager::instance()->ArePanelsLocked()); } void MainMenu::PopulateOpenRecent() diff --git a/app/window/mainwindow/mainmenu.h b/app/window/mainwindow/mainmenu.h index 2b6f3149b..de74d47fd 100644 --- a/app/window/mainwindow/mainmenu.h +++ b/app/window/mainwindow/mainmenu.h @@ -94,11 +94,6 @@ private slots: */ void WindowMenuAboutToShow(); - /** - * @brief Slot triggered just before the Window menu hides - */ - void WindowMenuAboutToHide(); - /** * @brief Adds items to open recent menu */ diff --git a/app/window/mainwindow/mainstatusbar.cpp b/app/window/mainwindow/mainstatusbar.cpp index 36a96d47b..568718e44 100644 --- a/app/window/mainwindow/mainstatusbar.cpp +++ b/app/window/mainwindow/mainstatusbar.cpp @@ -76,4 +76,11 @@ void MainStatusBar::UpdateStatus() } } +void MainStatusBar::mouseDoubleClickEvent(QMouseEvent* e) +{ + QStatusBar::mouseDoubleClickEvent(e); + + emit DoubleClicked(); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/window/mainwindow/mainstatusbar.h b/app/window/mainwindow/mainstatusbar.h index 1a704860e..96e8dee5e 100644 --- a/app/window/mainwindow/mainstatusbar.h +++ b/app/window/mainwindow/mainstatusbar.h @@ -33,11 +33,18 @@ OLIVE_NAMESPACE_ENTER */ class MainStatusBar : public QStatusBar { + Q_OBJECT public: MainStatusBar(QWidget* parent = nullptr); void ConnectTaskManager(TaskManager* manager); +signals: + void DoubleClicked(); + +protected: + virtual void mouseDoubleClickEvent(QMouseEvent* e) override; + private slots: void UpdateStatus(); diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index e034af473..008a90ccf 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -33,18 +33,21 @@ OLIVE_NAMESPACE_ENTER MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { -#ifdef Q_OS_WINDOWS - // Qt on Windows has a bug that "de-maximizes" the window when widgets are added, resizing the window beforehand - // works around that issue and we just set it to whatever size is available + // Resizes main window to desktop geometry on startup. Fixes the following issues: + // * Qt on Windows has a bug that "de-maximizes" the window when widgets are added, resizing the + // window beforehand works around that issue and we just set it to whatever size is available. + // * On Linux, it seems the window starts off at a vastly different size and then maximizes + // which throws off the proportions and makes the resulting layout wonky. resize(qApp->desktop()->availableGeometry(this).size()); +#ifdef Q_OS_WINDOWS // Set up taskbar button progress bar (used for some modal tasks like exporting) taskbar_btn_id_ = RegisterWindowMessage("TaskbarButtonCreated"); taskbar_interface_ = nullptr; #endif - // Create empty central widget - we don't actually want a central widget but some of Qt's docking/undocking fails - // without it + // Create empty central widget - we don't actually want a central widget (so we set its maximum + // size to 0,0) but some of Qt's docking/undocking fails without it QWidget* centralWidget = new QWidget(this); centralWidget->setMaximumSize(QSize(0, 0)); setCentralWidget(centralWidget); @@ -62,6 +65,7 @@ MainWindow::MainWindow(QWidget *parent) : // Create and set status bar MainStatusBar* status_bar = new MainStatusBar(this); status_bar->ConnectTaskManager(TaskManager::instance()); + connect(status_bar, &MainStatusBar::DoubleClicked, this, &MainWindow::StatusBarDoubleClicked); setStatusBar(status_bar); // Create standard panels @@ -390,6 +394,12 @@ bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, long *r } #endif +void MainWindow::StatusBarDoubleClicked() +{ + task_man_panel_->show(); + task_man_panel_->raise(); +} + void MainWindow::UpdateTitle() { if (Core::instance()->GetActiveProject()) { diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index cbf2ccc26..362b88ca4 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -150,6 +150,8 @@ private slots: void LoadLayoutInternal(QXmlStreamReader* reader, XMLNodeData *xml_data); + void StatusBarDoubleClicked(); + }; OLIVE_NAMESPACE_EXIT diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in new file mode 100644 index 000000000..ae08eb0d9 --- /dev/null +++ b/cmake/MacOSXBundleInfo.plist.in @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + NSPrincipalClass + NSApplication + +