diff --git a/.travis/script.sh b/.travis/script.sh index 9afa8e615..defeda5b2 100644 --- a/.travis/script.sh +++ b/.travis/script.sh @@ -29,6 +29,9 @@ if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then wget -c -nv https://github.com/arl/macdeployqtfix/raw/master/macdeployqtfix.py python2 macdeployqtfix.py $BUNDLE_NAME/Contents/MacOS/Olive /usr/local/Cellar/qt5/5.*/ + # Fix deps on crash handler + python2 macdeployqtfix.py $BUNDLE_NAME/Contents/MacOS/olive-crashhandler /usr/local/Cellar/qt5/5.*/ + # Fix OpenEXR libs that seem to be missed by both macdeployqt _and_ macdeployqtfix cd $BUNDLE_NAME/Contents/Frameworks exrlib=(libImath-*.dylib libHalf-*.dylib libIexMath-*.dylib libIex-*.dylib libIlmThread-*.dylib) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 03494d590..4cc08827f 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +# Set Olive sources and resources set(OLIVE_SOURCES ${OLIVE_SOURCES} core.h @@ -21,9 +22,12 @@ set(OLIVE_SOURCES main.cpp ) -set(OLIVE_RESOURCES - ${OLIVE_RESOURCES} -) +if (WIN32) + set(OLIVE_RESOURCES + ${OLIVE_RESOURCES} + packaging/windows/resources.rc + ) +endif() add_subdirectory(audio) add_subdirectory(codec) @@ -43,18 +47,20 @@ add_subdirectory(undo) add_subdirectory(widget) add_subdirectory(window) +# Create main application target set(OLIVE_TARGET "olive-editor") if(APPLE) set(OLIVE_TARGET "Olive") -endif() -if (WIN32) + set(OLIVE_ICON packaging/macos/olive.icns) + set(OLIVE_RESOURCES ${OLIVE_RESOURCES} - packaging/windows/resources.rc + ${OLIVE_ICON} ) endif() +# Add executable add_executable(${OLIVE_TARGET} ${OLIVE_SOURCES} ${OLIVE_RESOURCES} @@ -62,14 +68,18 @@ add_executable(${OLIVE_TARGET} ) if(APPLE) - SET_TARGET_PROPERTIES(${OLIVE_TARGET} PROPERTIES + set_target_properties(${OLIVE_TARGET} PROPERTIES MACOSX_BUNDLE TRUE - MACOSX_FRAMEWORK_IDENTIFIER org.olivevideoeditor.Olive + MACOSX_BUNDLE_GUI_IDENTIFIER org.olivevideoeditor.Olive + MACOSX_BUNDLE_ICON_FILE olive.icns + RESOURCE "${OLIVE_ICON}" ) endif() +# Set compiler definitions target_compile_definitions(${OLIVE_TARGET} PRIVATE ${OLIVE_DEFINITIONS}) +# Set compiler options if(MSVC) target_compile_options( ${OLIVE_TARGET} @@ -100,6 +110,7 @@ else() ) endif() +# Set include directories target_include_directories( ${OLIVE_TARGET} PRIVATE @@ -109,6 +120,7 @@ target_include_directories( ${OPENEXR_INCLUDE_DIRS} ) +# Set link libraries target_link_libraries( ${OLIVE_TARGET} PRIVATE @@ -148,11 +160,6 @@ else() qt5_add_translation(OLIVE_QM_FILES ${OLIVE_TS_FILES}) endif() -if(UNIX AND NOT APPLE) - install(TARGETS ${OLIVE_TARGET} RUNTIME DESTINATION bin) - install(FILES ${OLIVE_QM_FILES} DESTINATION share/olive-editor/ts) -endif() - add_subdirectory(packaging) if(DOXYGEN_FOUND) @@ -191,3 +198,14 @@ target_link_libraries( Qt5::Gui Qt5::Widgets ) + +if(UNIX AND NOT APPLE) + install(TARGETS ${OLIVE_TARGET} ${OLIVE_CRASH_TARGET} RUNTIME DESTINATION bin) +endif() + +if(APPLE) + # Move crash handler program inside Mac app bundle + add_custom_command(TARGET ${OLIVE_CRASH_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${OLIVE_CRASH_TARGET} $ + ) +endif() diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index df4ca479e..956c4a624 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -194,6 +194,7 @@ AudioManager::AudioManager() : RefreshDevices(); connect(&output_manager_, &AudioOutputManager::SentSamples, this, &AudioManager::SentSamples); + connect(&output_manager_, &AudioOutputManager::OutputNotified, this, &AudioManager::OutputNotified); output_manager_.SetEnableSendingSamples(true); } diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index 8a806652e..73e990114 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -102,6 +102,8 @@ signals: void SentSamples(QVector averages); + void OutputNotified(); + private: AudioManager(); diff --git a/app/audio/outputmanager.cpp b/app/audio/outputmanager.cpp index de27d92ac..8d601f07c 100644 --- a/app/audio/outputmanager.cpp +++ b/app/audio/outputmanager.cpp @@ -54,7 +54,7 @@ void AudioOutputManager::Push(const QByteArray& samples) ResetToPushMode(); // Start pushing samples to the output - OutputNotified(); + PushMoreSamples(); } void AudioOutputManager::ResetToPushMode() @@ -92,7 +92,7 @@ void AudioOutputManager::PullFromDevice(QIODevice *device, int playback_speed) output_->start(&device_proxy_); } -void AudioOutputManager::OutputNotified() +void AudioOutputManager::PushMoreSamples() { // Check if we're currently in push mode and if we have samples to push if (!push_device_ || pushed_samples_.isEmpty()) { @@ -139,6 +139,7 @@ void AudioOutputManager::SetOutputDevice(QAudioDeviceInfo info, QAudioFormat for output_ = std::unique_ptr(new QAudioOutput(info, format, this)); output_->setNotifyInterval(1); push_device_ = output_->start(); + connect(output_.get(), &QAudioOutput::notify, this, &AudioOutputManager::PushMoreSamples); connect(output_.get(), &QAudioOutput::notify, this, &AudioOutputManager::OutputNotified); } diff --git a/app/audio/outputmanager.h b/app/audio/outputmanager.h index 4b00208de..06024f4d1 100644 --- a/app/audio/outputmanager.h +++ b/app/audio/outputmanager.h @@ -69,6 +69,8 @@ signals: */ void SentSamples(QVector averages); + void OutputNotified(); + private: void ProcessAverages(const char* data, int length); @@ -83,7 +85,7 @@ private: AudioOutputDeviceProxy device_proxy_; private slots: - void OutputNotified(); + void PushMoreSamples(); }; #endif // AUDIOHYBRIDDEVICE_H diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index e92189472..60f61efe5 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -355,7 +355,7 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV } if (params().video_buffer_size() > 0) { - video_codec_ctx_->rc_buffer_size = params().video_buffer_size(); + video_codec_ctx_->rc_buffer_size = static_cast(params().video_buffer_size()); } } diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index 310e29d03..ea7daff5c 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -62,17 +62,23 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled) return false; } + is_sequence_ = false; + // Heuristically determine whether this file is part of an image sequence or not if (GetImageSequenceDigitCount(f->filename()) > 0) { - // We need user feedback here and since UI must occur in the UI thread (and we could be in any thread), we defer - // to the Core which will definitely be in the UI thread and block here until we get an answer from the user - QMetaObject::invokeMethod(Core::instance(), - "ConfirmImageSequence", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(bool, is_sequence_), - Q_ARG(QString, f->filename())); - } else { - is_sequence_ = false; + int64_t ind = GetImageSequenceIndex(f->filename()); + + // Check if files around exist around it with that follow a sequence + if (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), ind - 1)) + || QFileInfo::exists(TransformImageSequenceFileName(f->filename(), ind + 1))) { + // We need user feedback here and since UI must occur in the UI thread (and we could be in any thread), we defer + // to the Core which will definitely be in the UI thread and block here until we get an answer from the user + QMetaObject::invokeMethod(Core::instance(), + "ConfirmImageSequence", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, is_sequence_), + Q_ARG(QString, f->filename())); + } } ImageStreamPtr image_stream; @@ -84,12 +90,25 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled) rational default_timebase = Config::Current()["DefaultSequenceFrameRate"].value(); video_stream->set_timebase(default_timebase); video_stream->set_frame_rate(default_timebase.flipped()); + video_stream->set_image_sequence(true); - // FIXME: Get actual start number - video_stream->set_start_time(1); + int64_t seq_index = GetImageSequenceIndex(f->filename()); - // FIXME: Get actual duration - video_stream->set_duration(200); + int64_t start_index = seq_index; + int64_t end_index = seq_index; + + // Heuristic to find the first and last images (users can always override this later in FootagePropertiesDialog) + while (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), start_index-1))) { + start_index--; + } + + while (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), end_index+1))) { + end_index++; + } + + video_stream->set_start_time(start_index); + + video_stream->set_duration(end_index - start_index); } else { image_stream = std::make_shared(); } @@ -260,6 +279,19 @@ QString OIIODecoder::TransformImageSequenceFileName(const QString &filename, con return file_info.dir().filePath(file_info.fileName().replace(original_basename, new_basename)); } +int64_t OIIODecoder::GetImageSequenceIndex(const QString &filename) +{ + int digit_count = GetImageSequenceDigitCount(filename); + + QFileInfo file_info(filename); + + QString original_basename = file_info.baseName(); + + QString number_only = original_basename.mid(original_basename.size() - digit_count); + + return number_only.toLongLong(); +} + bool OIIODecoder::OpenImageHandler(const QString &fn) { image_ = OIIO::ImageInput::open(fn.toStdString()); diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index f4eff4db6..657758bd8 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -58,6 +58,8 @@ private: static QString TransformImageSequenceFileName(const QString& filename, const int64_t& number); + static int64_t GetImageSequenceIndex(const QString& filename); + bool OpenImageHandler(const QString& fn); void CloseImageHandle(); diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index c02c8acf2..e08078af0 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -45,5 +45,7 @@ set(OLIVE_SOURCES common/timelinecommon.h common/timerange.h common/timerange.cpp + common/xmlutils.h + common/xmlutils.cpp PARENT_SCOPE ) diff --git a/app/common/crashhandler.cpp b/app/common/crashhandler.cpp index 4844c9736..27958f543 100644 --- a/app/common/crashhandler.cpp +++ b/app/common/crashhandler.cpp @@ -13,7 +13,7 @@ #include #include #include -#elif defined(Q_OS_LINUX) +#elif defined(Q_OS_MAC) || defined(Q_OS_LINUX) #include #endif @@ -100,9 +100,7 @@ void crash_handler(int sig) { } SymCleanup(process); -#elif defined(Q_OS_MAC) - // FIXME: No Mac backtrace support yet -#elif defined(Q_OS_LINUX) +#elif defined(Q_OS_MAC) || defined(Q_OS_LINUX) void *array[10]; size_t size; diff --git a/app/common/rational.h b/app/common/rational.h index 59ed8775e..505ac313f 100644 --- a/app/common/rational.h +++ b/app/common/rational.h @@ -118,6 +118,7 @@ private: QDebug operator<<(QDebug debug, const rational& r); +// We define these limits at 32-bit to try avoiding integer overflow #define RATIONAL_MIN rational(INT32_MIN, 1) #define RATIONAL_MAX rational(INT32_MAX, 1) diff --git a/app/common/timecodefunctions.cpp b/app/common/timecodefunctions.cpp index c2725290b..86c9eaf36 100644 --- a/app/common/timecodefunctions.cpp +++ b/app/common/timecodefunctions.cpp @@ -258,13 +258,3 @@ int64_t Timecode::time_to_timestamp(const double &time, const rational &timebase { return qRound64(time * timebase.flipped().toDouble()); } - -Timecode::Display Timecode::CurrentDisplay() -{ - return static_cast(Config::Current()["TimecodeDisplay"].toInt()); -} - -void Timecode::SetCurrentDisplay(Timecode::Display d) -{ - Config::Current()["TimecodeDisplay"] = d; -} diff --git a/app/common/timecodefunctions.h b/app/common/timecodefunctions.h index e9bb4d35d..0d1748394 100644 --- a/app/common/timecodefunctions.h +++ b/app/common/timecodefunctions.h @@ -45,9 +45,6 @@ public: kMilliseconds }; - static Display CurrentDisplay(); - static void SetCurrentDisplay(Display d); - /** * @brief Convert a timestamp (according to a rational timebase) to a user-friendly string representation */ diff --git a/app/common/xmlreadloop.h b/app/common/xmlreadloop.h deleted file mode 100644 index 80e101c5f..000000000 --- a/app/common/xmlreadloop.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef XMLREADLOOP_H -#define XMLREADLOOP_H - -#define XMLReadLoop(reader, section) \ - while (!reader->atEnd() && !(reader->name() == section && reader->isEndElement()) && reader->readNext()) - -#define XMLAttributeLoop(reader, item) \ - QXmlStreamAttributes __attributes = reader->attributes(); \ - foreach (const QXmlStreamAttribute& item, __attributes) - -#endif // XMLREADLOOP_H diff --git a/app/common/xmlutils.cpp b/app/common/xmlutils.cpp new file mode 100644 index 000000000..790abe546 --- /dev/null +++ b/app/common/xmlutils.cpp @@ -0,0 +1,56 @@ +#include "xmlutils.h" + +#include "node/factory.h" + +Node* XMLLoadNode(QXmlStreamReader* reader) { + QString node_id; + + XMLAttributeLoop(reader, attr) { + if (attr.name() == "id") { + node_id = attr.value().toString(); + + // Currently the only thing we need + break; + } + } + + if (node_id.isEmpty()) { + qWarning() << "Found node with no ID"; + return nullptr; + } + + Node* node = NodeFactory::CreateFromID(node_id); + + if (!node) { + qWarning() << "Failed to load" << node_id << "- no node with that ID is installed"; + } + + return node; +} + +void XMLConnectNodes(const QHash& output_ptrs, const QList& desired_connections) +{ + foreach (const NodeParam::SerializedConnection& con, desired_connections) { + NodeOutput* out = output_ptrs.value(con.output); + + if (out) { + NodeParam::ConnectEdge(out, con.input); + } + } +} + +bool XMLReadNextStartElement(QXmlStreamReader *reader) +{ + QXmlStreamReader::TokenType token; + + while ((token = reader->readNext()) != QXmlStreamReader::Invalid + && token != QXmlStreamReader::EndDocument) { + if (reader->isEndElement()) { + return false; + } else if (reader->isStartElement()) { + return true; + } + } + + return false; +} diff --git a/app/common/xmlutils.h b/app/common/xmlutils.h new file mode 100644 index 000000000..d3530abb3 --- /dev/null +++ b/app/common/xmlutils.h @@ -0,0 +1,18 @@ +#ifndef XMLREADLOOP_H +#define XMLREADLOOP_H + +#include + +#include "node/node.h" + +#define XMLAttributeLoop(reader, item) \ + QXmlStreamAttributes __attributes = reader->attributes(); \ + foreach (const QXmlStreamAttribute& item, __attributes) + +Node *XMLLoadNode(QXmlStreamReader* reader); + +void XMLConnectNodes(const QHash &output_ptrs, const QList &desired_connections); + +bool XMLReadNextStartElement(QXmlStreamReader* reader); + +#endif // XMLREADLOOP_H diff --git a/app/config/config.cpp b/app/config/config.cpp index 0d22fc54e..88d80d4c0 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -29,6 +29,7 @@ #include "common/autoscroll.h" #include "common/filefunctions.h" +#include "common/xmlutils.h" #include "core.h" #include "window/mainwindow/mainwindow.h" @@ -75,6 +76,8 @@ void Config::SetDefaults() config_map_["Autoscroll"] = AutoScroll::kPage; config_map_["DefaultViewerDivider"] = 2; config_map_["AutoSelectDivider"] = false; + config_map_["SetNameWithMarker"] = false; + config_map_["RectifiedWaveforms"] = false; config_map_["DropWithoutSequenceBehavior"] = TimelineWidget::kDWSAsk; config_map_["DiskCachePath"] = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); @@ -119,52 +122,50 @@ void Config::Load() QString config_version; - while (!reader.atEnd()) { - reader.readNext(); + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("Configuration")) { + while (XMLReadNextStartElement(&reader)) { + QString key = reader.name().toString(); + QString value = reader.readElementText(); - if (!reader.isStartElement()) { - continue; - } + if (key == QStringLiteral("Version")) { + config_version = value; - QString key = reader.name().toString(); + if (!value.contains(".")) { + qDebug() << "CONFIG: This is a 0.1.x config file, upconvert"; + } + } else if (key == QStringLiteral("DefaultSequenceFrameRate") && !config_version.contains('.')) { + // 0.1.x stored this value as a float while we now use rationals, we'll use a heuristic to find the closest + // supported rational + qDebug() << " CONFIG: Finding closest match to" << value; - reader.readNext(); - QString value = reader.text().toString(); + double config_fr = value.toDouble(); - if (key == "Configuration") { - // First element, ignore - } else if (key == "Version") { - config_version = value; + QList supported_frame_rates = Core::SupportedFrameRates(); - if (!value.contains(".")) { - qDebug() << "CONFIG: This is a 0.1.x config file, upconvert"; - } - } else if (key == "DefaultSequenceFrameRate" && !config_version.contains(".")) { - // 0.1.x stored this value as a float while we now use rationals, we'll use a heuristic to find the closest - // supported rational - qDebug() << " CONFIG: Finding closest match to" << value; + rational match = supported_frame_rates.first(); + double match_diff = qAbs(match.toDouble() - config_fr); - double config_fr = value.toDouble(); + for (int i=1;i supported_frame_rates = Core::SupportedFrameRates(); + if (diff < match_diff) { + match = supported_frame_rates.at(i); + match_diff = diff; + } + } - rational match = supported_frame_rates.first(); - double match_diff = qAbs(match.toDouble() - config_fr); + qDebug() << " CONFIG: Closest match was" << match.toDouble(); - for (int i=1;imain_window(), QCoreApplication::translate("Config", "Error loading settings"), QCoreApplication::translate("Config", "Failed to load application settings. This session will " - "use defaults."), + "use defaults.\n\n%1").arg(reader.errorString()), QMessageBox::Ok); current_config_.SetDefaults(); } diff --git a/app/core.cpp b/app/core.cpp index d59a38092..e2cf27a53 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -482,6 +482,18 @@ Folder *Core::GetSelectedFolderInActiveProject() } } +Timecode::Display Core::GetTimecodeDisplay() const +{ + return static_cast(Config::Current()["TimecodeDisplay"].toInt()); +} + +void Core::SetTimecodeDisplay(Timecode::Display d) +{ + Config::Current()["TimecodeDisplay"] = d; + + emit TimecodeDisplayChanged(d); +} + void Core::SetProjectModified(bool e) { main_window()->setWindowModified(e); diff --git a/app/core.h b/app/core.h index eb17fb15a..1d0988485 100644 --- a/app/core.h +++ b/app/core.h @@ -26,6 +26,7 @@ #include #include "common/rational.h" +#include "common/timecodefunctions.h" #include "project/item/footage/footage.h" #include "project/item/sequence/sequence.h" #include "project/project.h" @@ -125,6 +126,16 @@ public: ProjectViewModel* GetActiveProjectModel(); Folder* GetSelectedFolderInActiveProject(); + /** + * @brief Gets current timecode display mode + */ + Timecode::Display GetTimecodeDisplay() const; + + /** + * @brief Sets current timecode display mode + */ + void SetTimecodeDisplay(Timecode::Display d); + /** * @brief Sets state to "modified" so that the GUI will prompt the user to save before closing * @@ -269,6 +280,11 @@ signals: */ void SnappingChanged(const bool& b); + /** + * @brief Signal emitted when the default timecode display mode changed + */ + void TimecodeDisplayChanged(Timecode::Display d); + private: /** * @brief Get the file filter than can be used with QFileDialog to open and save compatible projects diff --git a/app/dialog/footageproperties/footageproperties.cpp b/app/dialog/footageproperties/footageproperties.cpp index fa5063540..37f2dc467 100644 --- a/app/dialog/footageproperties/footageproperties.cpp +++ b/app/dialog/footageproperties/footageproperties.cpp @@ -87,15 +87,26 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *foota connect(track_list, SIGNAL(currentRowChanged(int)), stacked_widget_, SLOT(setCurrentIndex(int))); - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttons->setCenterButtons(true); layout->addWidget(buttons, row, 0, 1, 2); - connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttons, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); } void FootagePropertiesDialog::accept() { + // Perform sanity check on all pages + for (int i=0;icount();i++) { + if (!static_cast(stacked_widget_->widget(i))->SanityCheck()) { + // Switch to the failed panel in question + stacked_widget_->setCurrentIndex(i); + + // Do nothing (it's up to the property panel itself to throw the error message) + return; + } + } + QUndoCommand* command = new QUndoCommand(); if (footage_->name() != footage_name_field_->text()) { diff --git a/app/dialog/footageproperties/streamproperties/streamproperties.h b/app/dialog/footageproperties/streamproperties/streamproperties.h index 72840f010..30f8d7aa5 100644 --- a/app/dialog/footageproperties/streamproperties/streamproperties.h +++ b/app/dialog/footageproperties/streamproperties/streamproperties.h @@ -30,6 +30,9 @@ public: StreamProperties(QWidget* parent = nullptr); virtual void Accept(QUndoCommand*){} + + virtual bool SanityCheck(){return true;} + }; #endif // STREAMPROPERTIES_H diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index 333879e67..b4362c7d8 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -21,7 +21,9 @@ #include "videostreamproperties.h" #include +#include #include +#include #include namespace OCIO = OCIO_NAMESPACE::v1; @@ -35,12 +37,16 @@ VideoStreamProperties::VideoStreamProperties(ImageStreamPtr stream) : QGridLayout* video_layout = new QGridLayout(this); video_layout->setMargin(0); - video_layout->addWidget(new QLabel(tr("Color Space:")), 0, 0); + int row = 0; + + video_layout->addWidget(new QLabel(tr("Color Space:")), row, 0); video_color_space_ = new QComboBox(); OCIO::ConstConfigRcPtr config = stream->footage()->project()->color_manager()->GetConfig(); int number_of_colorspaces = config->getNumColorSpaces(); + video_color_space_->addItem(tr("Default (%1)").arg(stream->footage()->project()->default_input_colorspace())); + for (int i=0;igetColorSpaceNameByIndex(i); @@ -49,22 +55,94 @@ VideoStreamProperties::VideoStreamProperties(ImageStreamPtr stream) : video_color_space_->setCurrentText(stream_->colorspace()); - video_layout->addWidget(video_color_space_, 0, 1); + video_layout->addWidget(video_color_space_, row, 1); + + row++; video_premultiply_alpha_ = new QCheckBox(tr("Premultiplied Alpha")); video_premultiply_alpha_->setChecked(stream_->premultiplied_alpha()); - video_layout->addWidget(video_premultiply_alpha_, 1, 0, 1, 2); + video_layout->addWidget(video_premultiply_alpha_, row, 0, 1, 2); + + row++; + + if (IsImageSequence(stream.get())) { + QGroupBox* imgseq_group = new QGroupBox(tr("Image Sequence")); + QGridLayout* imgseq_layout = new QGridLayout(imgseq_group); + + int imgseq_row = 0; + + VideoStream* video_stream = static_cast(stream.get()); + + imgseq_layout->addWidget(new QLabel(tr("Start Index:")), imgseq_row, 0); + + imgseq_start_time_ = new IntegerSlider(); + imgseq_start_time_->SetMinimum(0); + imgseq_start_time_->SetValue(video_stream->start_time()); + imgseq_layout->addWidget(imgseq_start_time_, imgseq_row, 1); + + imgseq_row++; + + imgseq_layout->addWidget(new QLabel(tr("End Index:")), imgseq_row, 0); + + imgseq_end_time_ = new IntegerSlider(); + imgseq_end_time_->SetMinimum(0); + imgseq_end_time_->SetValue(video_stream->start_time() + video_stream->duration()); + imgseq_layout->addWidget(imgseq_end_time_, imgseq_row, 1); + + video_layout->addWidget(imgseq_group, row, 0, 1, 2); + } } void VideoStreamProperties::Accept(QUndoCommand *parent) { + QString set_colorspace; + + if (video_color_space_->currentIndex() > 0) { + set_colorspace = video_color_space_->currentText(); + } + if (video_premultiply_alpha_->isChecked() != stream_->premultiplied_alpha() - || video_color_space_->currentText() != stream_->colorspace()) { + || set_colorspace != stream_->colorspace(false)) { + new VideoStreamChangeCommand(stream_, video_premultiply_alpha_->isChecked(), - video_color_space_->currentText(), + set_colorspace, parent); } + + if (IsImageSequence(stream_.get())) { + VideoStreamPtr video_stream = std::static_pointer_cast(stream_); + + int64_t new_dur = imgseq_end_time_->GetValue() - imgseq_start_time_->GetValue(); + + if (video_stream->start_time() != imgseq_start_time_->GetValue() + || video_stream->duration() != new_dur) { + new ImageSequenceChangeCommand(video_stream, + imgseq_start_time_->GetValue(), + new_dur, + parent); + } + } +} + +bool VideoStreamProperties::SanityCheck() +{ + if (IsImageSequence(stream_.get())) { + if (imgseq_start_time_->GetValue() >= imgseq_end_time_->GetValue()) { + QMessageBox::critical(this, + tr("Invalid Configuration"), + tr("Image sequence end index must be a value higher than the start index."), + QMessageBox::Ok); + return false; + } + } + + return true; +} + +bool VideoStreamProperties::IsImageSequence(ImageStream *stream) +{ + return (stream->type() == Stream::kVideo && static_cast(stream)->is_image_sequence()); } VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(ImageStreamPtr stream, @@ -81,7 +159,7 @@ VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(ImageS void VideoStreamProperties::VideoStreamChangeCommand::redo_internal() { old_premultiplied_ = stream_->premultiplied_alpha(); - old_colorspace_ = stream_->colorspace(); + old_colorspace_ = stream_->colorspace(false); stream_->set_premultiplied_alpha(new_premultiplied_); stream_->set_colorspace(new_colorspace_); @@ -92,3 +170,26 @@ void VideoStreamProperties::VideoStreamChangeCommand::undo_internal() stream_->set_premultiplied_alpha(old_premultiplied_); stream_->set_colorspace(old_colorspace_); } + +VideoStreamProperties::ImageSequenceChangeCommand::ImageSequenceChangeCommand(VideoStreamPtr video_stream, int64_t start_index, int64_t duration, QUndoCommand *parent) : + UndoCommand(parent), + video_stream_(video_stream), + new_start_index_(start_index), + new_duration_(duration) +{ +} + +void VideoStreamProperties::ImageSequenceChangeCommand::redo_internal() +{ + old_start_index_ = video_stream_->start_time(); + video_stream_->set_start_time(new_start_index_); + + old_duration_ = video_stream_->duration(); + video_stream_->set_duration(new_duration_); +} + +void VideoStreamProperties::ImageSequenceChangeCommand::undo_internal() +{ + video_stream_->set_start_time(old_start_index_); + video_stream_->set_duration(old_duration_); +} diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h index e59bfb84f..183ab7e32 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -27,6 +27,7 @@ #include "project/item/footage/videostream.h" #include "streamproperties.h" #include "undo/undocommand.h" +#include "widget/slider/integerslider.h" class VideoStreamProperties : public StreamProperties { @@ -35,7 +36,11 @@ public: virtual void Accept(QUndoCommand* parent) override; + virtual bool SanityCheck() override; + private: + static bool IsImageSequence(ImageStream* stream); + /** * @brief Attached video stream */ @@ -51,6 +56,16 @@ private: */ QComboBox* video_color_space_; + /** + * @brief Sets the start index for image sequences + */ + IntegerSlider* imgseq_start_time_; + + /** + * @brief Sets the end index for image sequences + */ + IntegerSlider* imgseq_end_time_; + class VideoStreamChangeCommand : public UndoCommand { public: VideoStreamChangeCommand(ImageStreamPtr stream, @@ -70,6 +85,29 @@ private: bool old_premultiplied_; QString old_colorspace_; + + }; + + class ImageSequenceChangeCommand : public UndoCommand { + public: + ImageSequenceChangeCommand(VideoStreamPtr video_stream, + int64_t start_index, + int64_t duration, + QUndoCommand* parent = nullptr); + + protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + + private: + VideoStreamPtr video_stream_; + + int64_t new_start_index_; + int64_t old_start_index_; + + int64_t new_duration_; + int64_t old_duration_; + }; }; diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp index 4d448c91d..59ab3198a 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp @@ -2,8 +2,6 @@ #include #include -#include -#include #include #include "common/autoscroll.h" @@ -74,6 +72,14 @@ PreferencesGeneralTab::PreferencesGeneralTab() row++; + general_layout->addWidget(new QLabel(tr("Rectified Waveforms:")), row, 0); + + rectified_waveforms_ = new QCheckBox(); + rectified_waveforms_->setChecked(Config::Current()["RectifiedWaveforms"].toBool()); + general_layout->addWidget(rectified_waveforms_, row, 1); + + row++; + general_layout->addWidget(new QLabel(tr("Default Still Image Length:")), row, 0); default_still_length_ = new FloatSlider(); @@ -101,6 +107,8 @@ void PreferencesGeneralTab::Accept() Config::Current()["DefaultSequenceAudioFrequency"] = default_sequence_.audio_params().sample_rate(); Config::Current()["DefaultSequenceAudioLayout"] = QVariant::fromValue(default_sequence_.audio_params().channel_layout()); + Config::Current()["RectifiedWaveforms"] = rectified_waveforms_->isChecked(); + Config::Current()["Autoscroll"] = autoscroll_method_->currentData(); Config::Current()["DefaultStillLength"] = QVariant::fromValue(rational::fromDouble(default_still_length_->GetValue())); diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.h b/app/dialog/preferences/tabs/preferencesgeneraltab.h index b05633040..f6d6d8f2a 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.h +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.h @@ -1,6 +1,7 @@ #ifndef PREFERENCESGENERALTAB_H #define PREFERENCESGENERALTAB_H +#include #include #include @@ -27,6 +28,8 @@ private: QComboBox* autoscroll_method_; + QCheckBox* rectified_waveforms_; + FloatSlider* default_still_length_; /** diff --git a/app/node/CMakeLists.txt b/app/node/CMakeLists.txt index 68bb5194f..bb8f9a42f 100644 --- a/app/node/CMakeLists.txt +++ b/app/node/CMakeLists.txt @@ -46,6 +46,8 @@ set(OLIVE_SOURCES node/output.cpp node/param.h node/param.cpp + node/traverser.h + node/traverser.cpp node/value.h node/value.cpp PARENT_SCOPE diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 312f10103..69cda6126 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -202,22 +202,28 @@ void Block::LengthInputChanged() emit LengthChanged(length()); } -void Block::Link(Block *a, Block *b) +bool Block::Link(Block *a, Block *b) { - if (a == b || a == nullptr || b == nullptr) { - return; + if (a == b || !a || !b) { + return false; } - // Assume both clips are already linked since Link() and Unlink() should be the only entry points to this array - if (a->linked_clips_.contains(b)) { - return; + // Prevent duplicate link entries (assume that we only need to check one clip since this should be the only function + // that adds to the linked array) + if (Block::AreLinked(a, b)) { + return false; } a->linked_clips_.append(b); b->linked_clips_.append(a); + + emit a->LinksChanged(); + emit b->LinksChanged(); + + return true; } -void Block::Link(QList blocks) +void Block::Link(const QList& blocks) { foreach (Block* a, blocks) { foreach (Block* b, blocks) { @@ -226,10 +232,32 @@ void Block::Link(QList blocks) } } -void Block::Unlink(Block *a, Block *b) +bool Block::Unlink(Block *a, Block *b) { + if (a == b || !a || !b) { + return false; + } + + if (!Block::AreLinked(a, b)) { + return false; + } + a->linked_clips_.removeOne(b); b->linked_clips_.removeOne(a); + + emit a->LinksChanged(); + emit b->LinksChanged(); + + return true; +} + +void Block::Unlink(const QList &blocks) +{ + foreach (Block* a, blocks) { + foreach (Block* b, blocks) { + Unlink(a, b); + } + } } bool Block::AreLinked(Block *a, Block *b) diff --git a/app/node/block/block.h b/app/node/block/block.h index e0e43f1fe..cb9ea4e93 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -76,9 +76,10 @@ public: QString block_name() const; void set_block_name(const QString& name); - static void Link(Block* a, Block* b); - static void Link(QList blocks); - static void Unlink(Block* a, Block* b); + static bool Link(Block* a, Block* b); + static void Link(const QList& blocks); + static bool Unlink(Block* a, Block* b); + static void Unlink(const QList& blocks); static bool AreLinked(Block* a, Block* b); const QVector& linked_clips(); bool HasLinks(); @@ -103,6 +104,8 @@ signals: void LengthChanged(const rational& length); + void LinksChanged(); + protected: rational SequenceToMediaTime(const rational& sequence_time) const; diff --git a/app/node/input.cpp b/app/node/input.cpp index c4a5281f6..8602ab06e 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -26,7 +26,7 @@ #include "common/bezier.h" #include "common/lerp.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "node.h" #include "output.h" #include "inputarray.h" @@ -91,111 +91,111 @@ void NodeInput::Load(QXmlStreamReader *reader, QHash& par return; } - if (attr.name() == "keyframing") { - set_is_keyframing(attr.value() == "1"); + if (attr.name() == QStringLiteral("keyframing")) { + set_is_keyframing(attr.value() == QStringLiteral("1")); } } - XMLReadLoop(reader, "input") { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - if (reader->name() == "standard") { - // Load standard value - int val_index = 0; + if (reader->name() == QStringLiteral("standard")) { + // Load standard value + int val_index = 0; - XMLReadLoop(reader, "standard") { - if (cancelled && *cancelled) { - return; + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; + } + + if (reader->name() == QStringLiteral("value")) { + QString value_text = reader->readElementText(); + + if (value_text.isEmpty()) { + standard_value_.replace(val_index, QVariant()); + } else { + standard_value_.replace(val_index, StringToValue(value_text, footage_connections)); } - if (reader->isStartElement() && reader->name() == "value") { - reader->readNext(); + val_index++; + } else { + reader->skipCurrentElement(); + } + } + } else if (reader->name() == QStringLiteral("keyframes")) { + int track = 0; - QString value_text = reader->text().toString(); + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; + } - if (value_text.isEmpty()) { - standard_value_.replace(val_index, QVariant()); - } else { - standard_value_.replace(val_index, StringToValue(value_text, footage_connections)); + if (reader->name() == QStringLiteral("track")) { + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; } - val_index++; - } - } - } else if (reader->name() == "keyframes") { - int track = 0; + if (reader->name() == QStringLiteral("key")) { + rational key_time; + NodeKeyframe::Type key_type; + QVariant key_value; + QPointF key_in_handle; + QPointF key_out_handle; - XMLReadLoop(reader, "keyframes") { - if (cancelled && *cancelled) { - return; - } - - if (reader->isStartElement() && reader->name() == "track") { - XMLReadLoop(reader, "track") { - if (cancelled && *cancelled) { - return; - } - - if (reader->name() == "key") { - rational key_time; - NodeKeyframe::Type key_type; - QVariant key_value; - QPointF key_in_handle; - QPointF key_out_handle; - - XMLAttributeLoop(reader, attr) { - if (cancelled && *cancelled) { - return; - } - - if (attr.name() == "time") { - key_time = rational::fromString(attr.value().toString()); - } else if (attr.name() == "type") { - key_type = static_cast(attr.value().toInt()); - } else if (attr.name() == "inhandlex") { - key_in_handle.setX(attr.value().toDouble()); - } else if (attr.name() == "inhandley") { - key_in_handle.setY(attr.value().toDouble()); - } else if (attr.name() == "outhandlex") { - key_out_handle.setX(attr.value().toDouble()); - } else if (attr.name() == "outhandley") { - key_out_handle.setY(attr.value().toDouble()); - } + XMLAttributeLoop(reader, attr) { + if (cancelled && *cancelled) { + return; } - reader->readNext(); - - key_value = StringToValue(reader->text().toString(), footage_connections); - - NodeKeyframePtr key = NodeKeyframe::Create(key_time, key_value, key_type, track); - key->set_bezier_control_in(key_in_handle); - key->set_bezier_control_out(key_out_handle); - key->set_parent(this); - keyframe_tracks_[track].append(key); + if (attr.name() == QStringLiteral("time")) { + key_time = rational::fromString(attr.value().toString()); + } else if (attr.name() == QStringLiteral("type")) { + key_type = static_cast(attr.value().toInt()); + } else if (attr.name() == QStringLiteral("inhandlex")) { + key_in_handle.setX(attr.value().toDouble()); + } else if (attr.name() == QStringLiteral("inhandley")) { + key_in_handle.setY(attr.value().toDouble()); + } else if (attr.name() == QStringLiteral("outhandlex")) { + key_out_handle.setX(attr.value().toDouble()); + } else if (attr.name() == QStringLiteral("outhandley")) { + key_out_handle.setY(attr.value().toDouble()); + } } + + key_value = StringToValue(reader->readElementText(), footage_connections); + + NodeKeyframePtr key = NodeKeyframe::Create(key_time, key_value, key_type, track); + key->set_bezier_control_in(key_in_handle); + key->set_bezier_control_out(key_out_handle); + key->set_parent(this); + keyframe_tracks_[track].append(key); + } else { + reader->skipCurrentElement(); } - - track++; } + + track++; + } else { + reader->skipCurrentElement(); } - } else if (reader->name() == "connections") { - XMLReadLoop(reader, "connections") { - if (cancelled && *cancelled) { - return; - } - - if (reader->isStartElement() && reader->name() == "connection") { - reader->readNext(); - - input_connections.append({this, reader->text().toULongLong()}); - } - } - } else { - LoadInternal(reader, param_ptrs, input_connections, footage_connections, cancelled); } + } else if (reader->name() == QStringLiteral("connections")) { + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; + } + + if (reader->name() == QStringLiteral("connection")) { + input_connections.append({this, reader->readElementText().toULongLong()}); + } else { + reader->skipCurrentElement(); + } + } + } else { + LoadInternal(reader, param_ptrs, input_connections, footage_connections, cancelled); } } } @@ -268,8 +268,9 @@ const NodeParam::DataType &NodeInput::data_type() const return data_type_; } -void NodeInput::LoadInternal(QXmlStreamReader*, QHash&, QList&, QList&, const QAtomicInt*) +void NodeInput::LoadInternal(QXmlStreamReader* reader, QHash&, QList&, QList&, const QAtomicInt*) { + reader->skipCurrentElement(); } void NodeInput::SaveInternal(QXmlStreamWriter*) const diff --git a/app/node/input/media/audio/audio.cpp b/app/node/input/media/audio/audio.cpp index ccfb52f56..6f7992de9 100644 --- a/app/node/input/media/audio/audio.cpp +++ b/app/node/input/media/audio/audio.cpp @@ -28,8 +28,3 @@ QString AudioInput::Description() const { return tr("Import an audio footage stream."); } - -NodeValueTable AudioInput::Value(const NodeValueDatabase &value) const -{ - return value[footage_input_]; -} diff --git a/app/node/input/media/audio/audio.h b/app/node/input/media/audio/audio.h index 641b4f8de..86aa8d330 100644 --- a/app/node/input/media/audio/audio.h +++ b/app/node/input/media/audio/audio.h @@ -15,9 +15,6 @@ public: virtual QString Category() const override; virtual QString Description() const override; -protected: - virtual NodeValueTable Value(const NodeValueDatabase& value) const override; - private: }; diff --git a/app/node/input/media/media.cpp b/app/node/input/media/media.cpp index 29189a479..ea27b8c35 100644 --- a/app/node/input/media/media.cpp +++ b/app/node/input/media/media.cpp @@ -69,22 +69,18 @@ void MediaInput::FootageChanged() return; } - if (connected_footage_ != nullptr) { - if (connected_footage_->type() == Stream::kImage || connected_footage_->type() == Stream::kVideo) { - disconnect(connected_footage_.get(), SIGNAL(ColorSpaceChanged()), this, SLOT(FootageColorSpaceChanged())); - } + if (connected_footage_) { + disconnect(connected_footage_.get(), &Stream::ParametersChanged, this, &MediaInput::FootageParametersChanged); } connected_footage_ = new_footage; - if (connected_footage_ != nullptr) { - if (connected_footage_->type() == Stream::kImage || connected_footage_->type() == Stream::kVideo) { - connect(connected_footage_.get(), SIGNAL(ColorSpaceChanged()), this, SLOT(FootageColorSpaceChanged())); - } + if (connected_footage_) { + connect(connected_footage_.get(), &Stream::ParametersChanged, this, &MediaInput::FootageParametersChanged); } } -void MediaInput::FootageColorSpaceChanged() +void MediaInput::FootageParametersChanged() { InvalidateCache(0, RATIONAL_MAX, footage_input_); } diff --git a/app/node/input/media/media.h b/app/node/input/media/media.h index 0253cc231..a19459cae 100644 --- a/app/node/input/media/media.h +++ b/app/node/input/media/media.h @@ -48,7 +48,7 @@ protected: private slots: void FootageChanged(); - void FootageColorSpaceChanged(); + void FootageParametersChanged(); }; diff --git a/app/node/inputarray.cpp b/app/node/inputarray.cpp index e7d0e0e01..7fe1fc668 100644 --- a/app/node/inputarray.cpp +++ b/app/node/inputarray.cpp @@ -2,7 +2,7 @@ #include -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "node.h" NodeInputArray::NodeInputArray(const QString &id, const DataType &type, const QVariant &default_value) : @@ -166,13 +166,17 @@ void NodeInputArray::RemoveAt(int index) void NodeInputArray::LoadInternal(QXmlStreamReader *reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt* cancelled) { - if (reader->name() == "subparameters") { - XMLReadLoop(reader, "subparameters") { - if (reader->name() == "input") { + if (reader->name() == QStringLiteral("subparameters")) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("input")) { Append(); At(GetSize() - 1)->Load(reader, param_ptrs, input_connections, footage_connections, cancelled); + } else { + reader->skipCurrentElement(); } } + } else { + NodeInput::Load(reader, param_ptrs, input_connections, footage_connections, cancelled); } } diff --git a/app/node/node.cpp b/app/node/node.cpp index 38a3b6500..fb12a0093 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -24,7 +24,7 @@ #include #include -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" Node::Node() : can_be_deleted_(true) @@ -50,39 +50,39 @@ Node::~Node() } } -void Node::Load(QXmlStreamReader *reader, QHash &output_ptrs, QList& input_connections, QList& footage_connections, const QAtomicInt* cancelled, const QString& element) +void Node::Load(QXmlStreamReader *reader, QHash &output_ptrs, QList& input_connections, QList& footage_connections, const QAtomicInt* cancelled) { - XMLReadLoop(reader, (element.isEmpty() ? "node" : element)) { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - if (reader->name() == "input" || reader->name() == "output") { - QString param_id; + if (reader->name() == QStringLiteral("input") || reader->name() == QStringLiteral("output")) { + QString param_id; - XMLAttributeLoop(reader, attr) { - if (attr.name() == "id") { - param_id = attr.value().toString(); + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("id")) { + param_id = attr.value().toString(); - break; - } + break; } - - if (param_id.isEmpty()) { - qDebug() << "Found parameter with no ID"; - continue; - } - - NodeParam* param = GetParameterWithID(param_id); - - if (!param) { - qDebug() << "No parameter in" << id() << "with parameter" << param_id; - continue; - } - - param->Load(reader, output_ptrs, input_connections, footage_connections, cancelled); } + + if (param_id.isEmpty()) { + qDebug() << "Found parameter with no ID"; + continue; + } + + NodeParam* param = GetParameterWithID(param_id); + + if (!param) { + qDebug() << "No parameter in" << id() << "with parameter" << param_id; + continue; + } + + param->Load(reader, output_ptrs, input_connections, footage_connections, cancelled); + } else { + reader->skipCurrentElement(); } } } diff --git a/app/node/node.h b/app/node/node.h index d14fef691..b049a8618 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -67,7 +67,7 @@ public: /** * @brief Clear current node variables and replace them with */ - void Load(QXmlStreamReader* reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt *cancelled, const QString &element = QString()); + void Load(QXmlStreamReader* reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt *cancelled); /** * @brief Save this node into a text/XML format diff --git a/app/node/output.cpp b/app/node/output.cpp index dd87ac9c1..79646932f 100644 --- a/app/node/output.cpp +++ b/app/node/output.cpp @@ -20,7 +20,7 @@ #include "output.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "node/node.h" NodeOutput::NodeOutput(const QString &id) : @@ -55,6 +55,8 @@ void NodeOutput::Load(QXmlStreamReader* reader, QHash& pa param_ptrs.insert(saved_ptr, this); } } + + reader->skipCurrentElement(); } void NodeOutput::Save(QXmlStreamWriter *writer) const diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index b86623419..f1327dcdd 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -20,6 +20,8 @@ #include "viewer.h" +#include "node/traverser.h" + ViewerOutput::ViewerOutput() { texture_input_ = new NodeInput("tex_in", NodeInput::kTexture); @@ -28,9 +30,6 @@ ViewerOutput::ViewerOutput() samples_input_ = new NodeInput("samples_in", NodeInput::kSamples); AddInput(samples_input_); - length_input_ = new NodeInput("length_in", NodeInput::kRational); - AddInput(length_input_); - // Create TrackList instances track_inputs_.resize(Timeline::kTrackTypeCount); track_lists_.resize(Timeline::kTrackTypeCount); @@ -91,11 +90,6 @@ NodeInput *ViewerOutput::samples_input() const return samples_input_; } -NodeInput *ViewerOutput::length_input() const -{ - return length_input_; -} - void ViewerOutput::InvalidateCache(const rational &start_range, const rational &end_range, NodeInput *from) { Node::InvalidateCache(start_range, end_range, from); @@ -104,8 +98,6 @@ void ViewerOutput::InvalidateCache(const rational &start_range, const rational & emit VideoChangedBetween(TimeRange(start_range, end_range)); } else if (from == samples_input()) { emit AudioChangedBetween(TimeRange(start_range, end_range)); - } else if (from == length_input()) { - emit LengthChanged(Length()); } SendInvalidateCache(start_range, end_range); @@ -146,18 +138,23 @@ void ViewerOutput::set_audio_params(const AudioParams &audio) rational ViewerOutput::Length() { - if (!length_input_->IsConnected()) { - return timeline_length_; + NodeTraverser traverser; + + rational video_length; + + if (texture_input_->IsConnected()) { + NodeValueTable t = traverser.ProcessNode(NodeDependency(texture_input_->get_connected_node(), 0, 0)); + video_length = t.Get(NodeParam::kNumber, "length").value(); } - Node* connected_node = length_input_->get_connected_node(); + rational audio_length; - if (connected_node) { - // This is kind of messy? - return connected_node->Value(NodeValueDatabase()).Get(NodeParam::kNumber, "length").value(); + if (samples_input_->IsConnected()) { + NodeValueTable t = traverser.ProcessNode(NodeDependency(samples_input_->get_connected_node(), 0, 0)); + audio_length = t.Get(NodeParam::kNumber, "length").value(); } - return 0; + return qMax(video_length, qMax(audio_length, timeline_length_)); } const QUuid &ViewerOutput::uuid() const diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index abdcd3f41..2d86dfc0e 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -52,7 +52,6 @@ public: NodeInput* texture_input() const; NodeInput* samples_input() const; - NodeInput* length_input() const; virtual void InvalidateCache(const rational &start_range, const rational &end_range, NodeInput *from = nullptr) override; virtual void InvalidateVisible(NodeInput *from) override; @@ -117,8 +116,6 @@ private: NodeInput* samples_input_; - NodeInput* length_input_; - VideoParams video_params_; AudioParams audio_params_; diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp new file mode 100644 index 000000000..fb3f28239 --- /dev/null +++ b/app/node/traverser.cpp @@ -0,0 +1,76 @@ +#include "traverser.h" + +#include "node.h" + +NodeTraverser::NodeTraverser() +{ + +} + +NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRange &range) +{ + NodeValueDatabase database; + + // We need to insert tables into the database for each input + foreach (NodeParam* param, node->parameters()) { + if (IsCancelled()) { + return NodeValueDatabase(); + } + + if (param->type() == NodeParam::kInput) { + NodeInput* input = static_cast(param); + TimeRange input_time = node->InputTimeAdjustment(input, range); + + NodeValueTable table = ProcessInput(input, input_time); + + InputProcessingEvent(input, input_time, &table); + + database.Insert(input, table); + } + } + + return database; +} + +NodeValueTable NodeTraverser::ProcessNode(const NodeDependency& dep) +{ + const Node* node = dep.node(); + + if (node->IsTrack()) { + // If the range is not wholly contained in this Block, we'll need to do some extra processing + return RenderBlock(static_cast(node), dep.range()); + } + + // FIXME: Cache certain values here if we've already processed them before + + // Generate database of input values of node + NodeValueDatabase database = GenerateDatabase(node, dep.range()); + + // By this point, the node should have all the inputs it needs to render correctly + NodeValueTable table = node->Value(database); + + ProcessNodeEvent(node, dep.range(), database, &table); + + return table; +} + +NodeValueTable NodeTraverser::RenderBlock(const TrackOutput *track, const TimeRange &range) +{ + // By default, don't bother traversing blocks + return NodeValueTable(); +} + +NodeValueTable NodeTraverser::ProcessInput(const NodeInput *input, const TimeRange& range) +{ + if (input->IsConnected()) { + // Value will equal something from the connected node, follow it + return ProcessNode(NodeDependency(input->get_connected_node(), range)); + } else { + // Push onto the table the value at this time from the input + QVariant input_value = input->get_value_at_time(range.in()); + + NodeValueTable table; + table.Push(input->data_type(), input_value); + return table; + } +} diff --git a/app/node/traverser.h b/app/node/traverser.h new file mode 100644 index 000000000..8166a7987 --- /dev/null +++ b/app/node/traverser.h @@ -0,0 +1,31 @@ +#ifndef NODETRAVERSER_H +#define NODETRAVERSER_H + +#include "codec/decoder.h" +#include "common/cancelableobject.h" +#include "dependency.h" +#include "node/output/track/track.h" +#include "project/item/footage/stream.h" +#include "value.h" + +class NodeTraverser : public CancelableObject +{ +public: + NodeTraverser(); + + NodeValueTable ProcessNode(const NodeDependency &dep); + +protected: + NodeValueDatabase GenerateDatabase(const Node *node, const TimeRange &range); + + virtual NodeValueTable RenderBlock(const TrackOutput *track, const TimeRange& range); + + NodeValueTable ProcessInput(const NodeInput* input, const TimeRange &range); + + virtual void InputProcessingEvent(NodeInput*, const TimeRange&, NodeValueTable*){} + + virtual void ProcessNodeEvent(const Node*, const TimeRange&, const NodeValueDatabase&, NodeValueTable*){} + +}; + +#endif // NODETRAVERSER_H diff --git a/app/node/value.cpp b/app/node/value.cpp index 2f5fc11a5..0ef541256 100644 --- a/app/node/value.cpp +++ b/app/node/value.cpp @@ -58,13 +58,9 @@ NodeValueTable::NodeValueTable() QVariant NodeValueTable::Get(const NodeParam::DataType &type, const QString &tag) const { - int value_index = GetInternal(type, tag); + NodeValue v = GetWithMeta(type, tag); - if (value_index >= 0) { - return values_.at(value_index).data(); - } - - return QVariant(); + return v.data(); } NodeValue NodeValueTable::GetWithMeta(const NodeParam::DataType &type, const QString &tag) const diff --git a/app/panel/node/node.cpp b/app/panel/node/node.cpp index d9be8e307..f21fe2098 100644 --- a/app/panel/node/node.cpp +++ b/app/panel/node/node.cpp @@ -59,6 +59,21 @@ void NodePanel::DeleteSelected() node_view_->DeleteSelected(); } +void NodePanel::CutSelected() +{ + node_view_->CopySelected(true); +} + +void NodePanel::CopySelected() +{ + node_view_->CopySelected(false); +} + +void NodePanel::Paste() +{ + node_view_->Paste(); +} + 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 e84d9c7c8..5ce5b75ca 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -40,6 +40,11 @@ public: virtual void DeleteSelected() override; + virtual void CutSelected() override; + virtual void CopySelected() override; + + virtual void Paste() override; + public slots: void Select(const QList& nodes); void SelectWithDependencies(const QList& nodes); diff --git a/app/panel/timebased/timebased.cpp b/app/panel/timebased/timebased.cpp index 2e76ae10f..e0e93e7a7 100644 --- a/app/panel/timebased/timebased.cpp +++ b/app/panel/timebased/timebased.cpp @@ -134,3 +134,33 @@ void TimeBasedPanel::Retranslate() SetSubtitle(tr("(none)")); } } + +void TimeBasedPanel::SetIn() +{ + GetTimeBasedWidget()->SetInAtPlayhead(); +} + +void TimeBasedPanel::SetOut() +{ + GetTimeBasedWidget()->SetOutAtPlayhead(); +} + +void TimeBasedPanel::ResetIn() +{ + GetTimeBasedWidget()->ResetIn(); +} + +void TimeBasedPanel::ResetOut() +{ + GetTimeBasedWidget()->ResetOut(); +} + +void TimeBasedPanel::ClearInOut() +{ + GetTimeBasedWidget()->ClearInOutPoints(); +} + +void TimeBasedPanel::SetMarker() +{ + GetTimeBasedWidget()->SetMarker(); +} diff --git a/app/panel/timebased/timebased.h b/app/panel/timebased/timebased.h index 59a4ec84e..db4c7f9c5 100644 --- a/app/panel/timebased/timebased.h +++ b/app/panel/timebased/timebased.h @@ -44,6 +44,18 @@ public: virtual void ShuttleRight() override; + virtual void SetIn() override; + + virtual void SetOut() override; + + virtual void ResetIn() override; + + virtual void ResetOut() override; + + virtual void ClearInOut() override; + + virtual void SetMarker() override; + public slots: void SetTimebase(const rational& timebase); diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 95dfdbb7d..53fd6116c 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -115,6 +115,11 @@ void TimelinePanel::Overwrite() } } +void TimelinePanel::ToggleLinks() +{ + static_cast(GetTimeBasedWidget())->ToggleLinksOnSelected(); +} + void TimelinePanel::InsertFootageAtPlayhead(const QList &footage) { static_cast(GetTimeBasedWidget())->InsertFootageAtPlayhead(footage); diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 02c09d1e5..3d5c1dd91 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -61,6 +61,8 @@ public: virtual void Overwrite() override; + virtual void ToggleLinks() override; + void InsertFootageAtPlayhead(const QList &footage); void OverwriteFootageAtPlayhead(const QList &footage); diff --git a/app/project/item/folder/folder.cpp b/app/project/item/folder/folder.cpp index 35a516332..12a221bd3 100644 --- a/app/project/item/folder/folder.cpp +++ b/app/project/item/folder/folder.cpp @@ -20,7 +20,7 @@ #include "folder.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "project/item/footage/footage.h" #include "project/item/sequence/sequence.h" #include "ui/icons/icons.h" @@ -46,37 +46,38 @@ QIcon Folder::icon() void Folder::Load(QXmlStreamReader *reader, QHash &footage_ptrs, QList& footage_connections, const QAtomicInt *cancelled) { + qDebug() << "Hello?"; + XMLAttributeLoop(reader, attr) { if (cancelled && *cancelled) { return; } - if (attr.name() == "name") { + if (attr.name() == QStringLiteral("name")) { set_name(attr.value().toString()); } } - XMLReadLoop(reader, "folder") { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - ItemPtr child; + ItemPtr child; - if (reader->name() == "folder") { - child = std::make_shared(); - } else if (reader->name() == "footage") { - child = std::make_shared(); - } else if (reader->name() == "sequence") { - child = std::make_shared(); - } else { - continue; - } - - add_child(child); - child->Load(reader, footage_ptrs, footage_connections, cancelled); + if (reader->name() == QStringLiteral("folder")) { + child = std::make_shared(); + } else if (reader->name() == QStringLiteral("footage")) { + child = std::make_shared(); + } else if (reader->name() == QStringLiteral("sequence")) { + child = std::make_shared(); + } else { + reader->skipCurrentElement(); + continue; } + + add_child(child); + child->Load(reader, footage_ptrs, footage_connections, cancelled); } } diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index 299597bf2..4434eb531 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -23,9 +23,9 @@ #include #include "codec/decoder.h" -#include "common/timecodefunctions.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "config/config.h" +#include "core.h" #include "ui/icons/icons.h" Footage::Footage() @@ -43,45 +43,45 @@ void Footage::Load(QXmlStreamReader *reader, QHash& footage QXmlStreamAttributes attributes = reader->attributes(); foreach (const QXmlStreamAttribute& attr, attributes) { - if (attr.name() == "name") { + if (attr.name() == QStringLiteral("name")) { set_name(attr.value().toString()); - } else if (attr.name() == "filename") { + } else if (attr.name() == QStringLiteral("filename")) { set_filename(attr.value().toString()); } } Decoder::ProbeMedia(this, cancelled); - XMLReadLoop(reader, "footage") { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - if (reader->name() == "stream") { - int stream_index = -1; - quintptr stream_ptr = 0; + if (reader->name() == QStringLiteral("stream")) { + int stream_index = -1; + quintptr stream_ptr = 0; - XMLAttributeLoop(reader, attr) { - if (cancelled && *cancelled) { - return; - } - - if (attr.name() == "index") { - stream_index = attr.value().toInt(); - } else if (attr.name() == "ptr") { - stream_ptr = attr.value().toULongLong(); - } + XMLAttributeLoop(reader, attr) { + if (cancelled && *cancelled) { + return; } - if (stream_index > -1 && stream_ptr > 0) { - footage_ptrs.insert(stream_ptr, stream(stream_index)); - - stream(stream_index)->Load(reader); - } else { - qWarning() << "Invalid stream found in project file"; + if (attr.name() == QStringLiteral("index")) { + stream_index = attr.value().toInt(); + } else if (attr.name() == QStringLiteral("ptr")) { + stream_ptr = attr.value().toULongLong(); } } + + if (stream_index > -1 && stream_ptr > 0) { + footage_ptrs.insert(stream_ptr, stream(stream_index)); + + stream(stream_index)->Load(reader); + } else { + qWarning() << "Invalid stream found in project file"; + } + } else { + reader->skipCurrentElement(); } } } @@ -232,12 +232,12 @@ QString Footage::duration() return Timecode::timestamp_to_timecode(duration, frame_rate_timebase, - Timecode::CurrentDisplay()); + Core::instance()->GetTimecodeDisplay()); } else if (streams_.first()->type() == Stream::kAudio) { AudioStreamPtr audio_stream = std::static_pointer_cast(streams_.first()); // If we're showing in a timecode, we prefer showing audio in seconds instead - Timecode::Display display = Timecode::CurrentDisplay(); + Timecode::Display display = Core::instance()->GetTimecodeDisplay(); if (display == Timecode::kTimecodeDropFrame || display == Timecode::kTimecodeNonDropFrame) { display = Timecode::kTimecodeSeconds; diff --git a/app/project/item/footage/footage.h b/app/project/item/footage/footage.h index 42226d64d..0e5f31ee9 100644 --- a/app/project/item/footage/footage.h +++ b/app/project/item/footage/footage.h @@ -30,6 +30,7 @@ #include "project/item/footage/audiostream.h" #include "project/item/footage/imagestream.h" #include "project/item/footage/videostream.h" +#include "timeline/timelinepoints.h" /** * @brief A reference to an external media file with metadata in a project structure @@ -38,7 +39,7 @@ * Footage objects store a list of Stream objects which store the majority of video/audio metadata. These streams * are identical to the stream data in the files. */ -class Footage : public Item +class Footage : public Item, public TimelinePoints { public: enum Status { diff --git a/app/project/item/footage/imagestream.cpp b/app/project/item/footage/imagestream.cpp index 4cc187623..2e17c0563 100644 --- a/app/project/item/footage/imagestream.cpp +++ b/app/project/item/footage/imagestream.cpp @@ -20,7 +20,7 @@ #include "imagestream.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "footage.h" #include "project/project.h" #include "render/colormanager.h" @@ -43,10 +43,11 @@ void ImageStream::FootageSetEvent(Footage *f) void ImageStream::LoadCustomParameters(QXmlStreamReader *reader) { - XMLReadLoop(reader, "stream") { - if (reader->isStartElement() && reader->name() == "colorspace") { - reader->readNext(); - set_colorspace(reader->text().toString()); + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("colorspace")) { + set_colorspace(reader->readElementText()); + } else { + reader->skipCurrentElement(); } } } @@ -91,11 +92,13 @@ bool ImageStream::premultiplied_alpha() const void ImageStream::set_premultiplied_alpha(bool e) { premultiplied_alpha_ = e; + + emit ParametersChanged(); } -const QString &ImageStream::colorspace() const +const QString &ImageStream::colorspace(bool default_if_empty) const { - if (colorspace_.isEmpty()) { + if (colorspace_.isEmpty() && default_if_empty) { return footage()->project()->default_input_colorspace(); } else { return colorspace_; @@ -106,7 +109,7 @@ void ImageStream::set_colorspace(const QString &color) { colorspace_ = color; - emit ColorSpaceChanged(); + emit ParametersChanged(); } void ImageStream::ColorConfigChanged() @@ -123,13 +126,13 @@ void ImageStream::ColorConfigChanged() } // Either way, the color calculation has likely changed so we signal here - emit ColorSpaceChanged(); + emit ParametersChanged(); } void ImageStream::DefaultColorSpaceChanged() { // If no colorspace is set, this stream uses the default color space and it's just changed if (colorspace_.isEmpty()) { - emit ColorSpaceChanged(); + emit ParametersChanged(); } } diff --git a/app/project/item/footage/imagestream.h b/app/project/item/footage/imagestream.h index 952b0c432..7c6adad2c 100644 --- a/app/project/item/footage/imagestream.h +++ b/app/project/item/footage/imagestream.h @@ -43,12 +43,9 @@ public: bool premultiplied_alpha() const; void set_premultiplied_alpha(bool e); - const QString& colorspace() const; + const QString& colorspace(bool default_if_empty = true) const; void set_colorspace(const QString& color); -signals: - void ColorSpaceChanged(); - protected: virtual void FootageSetEvent(Footage*) override; diff --git a/app/project/item/footage/stream.cpp b/app/project/item/footage/stream.cpp index c7f273271..4937481b2 100644 --- a/app/project/item/footage/stream.cpp +++ b/app/project/item/footage/stream.cpp @@ -107,6 +107,8 @@ const int64_t &Stream::duration() const void Stream::set_duration(const int64_t &duration) { duration_ = duration; + + emit ParametersChanged(); } bool Stream::enabled() const @@ -135,10 +137,10 @@ QIcon Stream::IconFromType(const Stream::Type &type) return QIcon(); } -StreamID Stream::ToID() const +/*StreamID Stream::ToID() const { return StreamID(footage_->filename(), index_); -} +}*/ QMutex* Stream::index_process_lock() { @@ -149,16 +151,17 @@ void Stream::FootageSetEvent(Footage*) { } -void Stream::LoadCustomParameters(QXmlStreamReader*) +void Stream::LoadCustomParameters(QXmlStreamReader* reader) { + reader->skipCurrentElement(); } void Stream::SaveCustomParameters(QXmlStreamWriter*) const { } -StreamID::StreamID(const QString &filename, const int &stream_index) : +/*StreamID::StreamID(const QString &filename, const int &stream_index) : filename_(filename), stream_index_(stream_index) { -} +}*/ diff --git a/app/project/item/footage/stream.h b/app/project/item/footage/stream.h index 0a0a7a319..743b9e6c4 100644 --- a/app/project/item/footage/stream.h +++ b/app/project/item/footage/stream.h @@ -31,7 +31,7 @@ class Footage; -class StreamID { +/*class StreamID { public: StreamID(const QString& filename, const int& stream_index); @@ -40,7 +40,7 @@ private: int stream_index_; -}; +};*/ /** * @brief A base class for keeping metadata about a media stream. @@ -101,7 +101,7 @@ public: static QIcon IconFromType(const Type& type); - StreamID ToID() const; + //StreamID ToID() const; QMutex* index_process_lock(); @@ -115,6 +115,8 @@ protected: signals: void IndexChanged(); + void ParametersChanged(); + private: Footage* footage_; diff --git a/app/project/item/footage/videostream.cpp b/app/project/item/footage/videostream.cpp index 5a0fb5df4..36d0f7fd5 100644 --- a/app/project/item/footage/videostream.cpp +++ b/app/project/item/footage/videostream.cpp @@ -27,7 +27,8 @@ const int64_t VideoStream::kEndTimestamp = AV_NOPTS_VALUE; VideoStream::VideoStream() : - start_time_(0) + start_time_(0), + is_image_sequence_(false) { set_type(kVideo); } @@ -57,6 +58,17 @@ const int64_t &VideoStream::start_time() const void VideoStream::set_start_time(const int64_t &start_time) { start_time_ = start_time; + emit ParametersChanged(); +} + +bool VideoStream::is_image_sequence() const +{ + return is_image_sequence_; +} + +void VideoStream::set_image_sequence(bool e) +{ + is_image_sequence_ = e; } int64_t VideoStream::get_closest_timestamp_in_frame_index(const rational &time) diff --git a/app/project/item/footage/videostream.h b/app/project/item/footage/videostream.h index 3fed4bfc4..05ec4ac95 100644 --- a/app/project/item/footage/videostream.h +++ b/app/project/item/footage/videostream.h @@ -25,6 +25,7 @@ class VideoStream : public ImageStream { + Q_OBJECT public: VideoStream(); @@ -43,6 +44,9 @@ public: const int64_t& start_time() const; void set_start_time(const int64_t& start_time); + bool is_image_sequence() const; + void set_image_sequence(bool e); + int64_t get_closest_timestamp_in_frame_index(const rational& time); int64_t get_closest_timestamp_in_frame_index(int64_t timestamp); void clear_frame_index(); @@ -62,6 +66,8 @@ private: QMutex index_access_lock_; + bool is_image_sequence_; + }; using VideoStreamPtr = std::shared_ptr; diff --git a/app/project/item/item.cpp b/app/project/item/item.cpp index 661708260..d694bc1e4 100644 --- a/app/project/item/item.cpp +++ b/app/project/item/item.cpp @@ -76,11 +76,19 @@ const QList &Item::children() const return children_; } -ItemPtr Item::shared_ptr_from_raw(Item *item) +ItemPtr Item::shared_ptr_from_raw(Item *item, bool traverse) { for (int i=0;iCanHaveChildren()) { + ItemPtr grandchild = shared_ptr_from_raw(item); + + if (grandchild) { + return grandchild; + } } } @@ -147,6 +155,23 @@ void Item::set_project(Project *project) project_ = project; } +QList Item::get_children_of_type(Type type, bool recursive) const +{ + QList list; + + foreach (ItemPtr item, children_) { + if (item->type() == type) { + list.append(item); + } + + if (recursive && item->CanHaveChildren()) { + list.append(item->get_children_of_type(type, recursive)); + } + } + + return list; +} + bool Item::CanHaveChildren() const { return false; diff --git a/app/project/item/item.h b/app/project/item/item.h index e8385f09f..42cb83bf7 100644 --- a/app/project/item/item.h +++ b/app/project/item/item.h @@ -77,7 +77,7 @@ public: Item* child(int i) const; const QList& children() const; - ItemPtr shared_ptr_from_raw(Item* item); + ItemPtr shared_ptr_from_raw(Item* item, bool traverse = false); const QString& name() const; void set_name(const QString& n); @@ -97,6 +97,8 @@ public: Project* project() const; void set_project(Project* project); + QList get_children_of_type(Type type, bool recursive) const; + virtual bool CanHaveChildren() const; bool ChildExistsWithName(const QString& name); diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index e8edb5601..00101f126 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -25,7 +25,7 @@ #include "config/config.h" #include "common/channellayout.h" #include "common/timecodefunctions.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "node/factory.h" #include "panel/panelmanager.h" #include "panel/node/node.h" @@ -59,97 +59,68 @@ void Sequence::Load(QXmlStreamReader *reader, QHash &, QLis QHash output_ptrs; QList desired_connections; - XMLReadLoop(reader, "sequence") { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - if (reader->name() == "video") { - int video_width, video_height; - rational video_timebase; + if (reader->name() == QStringLiteral("video")) { + int video_width, video_height; + rational video_timebase; - XMLReadLoop(reader, "video") { - if (cancelled && *cancelled) { - return; - } - - if (reader->isStartElement()) { - if (reader->name() == "width") { - reader->readNext(); - video_width = reader->text().toInt(); - } else if (reader->name() == "height") { - reader->readNext(); - video_height = reader->text().toInt(); - } else if (reader->name() == "timebase") { - reader->readNext(); - video_timebase = rational::fromString(reader->text().toString()); - } - } + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; } - set_video_params(VideoParams(video_width, video_height, video_timebase)); - } else if (reader->name() == "audio") { - int rate; - uint64_t layout; - - XMLReadLoop(reader, "audio") { - if (reader->isStartElement()) { - if (reader->name() == "rate") { - reader->readNext(); - rate = reader->text().toInt(); - } else if (reader->name() == "layout") { - reader->readNext(); - layout = reader->text().toULongLong(); - } - } - } - - set_audio_params(AudioParams(rate, layout)); - } else if (reader->name() == "node" || reader->name() == "viewer") { - Node* node; - - if (reader->name() == "node") { - QString node_id; - - XMLAttributeLoop(reader, attr) { - if (attr.name() == "id") { - node_id = attr.value().toString(); - - // Currently the only thing we need - break; - } - } - - if (node_id.isEmpty()) { - qDebug() << "Found node with no ID"; - continue; - } - - node = NodeFactory::CreateFromID(node_id); - - if (!node) { - qDebug() << "Failed to load" << node_id << "- no node with that ID is installed"; - continue; - } + if (reader->name() == QStringLiteral("width")) { + video_width = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("height")) { + video_height = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("timebase")) { + video_timebase = rational::fromString(reader->readElementText()); } else { - node = viewer_output_; - } - - if (node) { - node->Load(reader, output_ptrs, desired_connections, footage_connections, cancelled, reader->name().toString()); - - AddNode(node); + reader->skipCurrentElement(); } } + + set_video_params(VideoParams(video_width, video_height, video_timebase)); + } else if (reader->name() == QStringLiteral("audio")) { + int rate; + uint64_t layout; + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("rate")) { + rate = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("layout")) { + layout = reader->readElementText().toULongLong(); + } else { + reader->skipCurrentElement(); + } + } + + set_audio_params(AudioParams(rate, layout)); + } else if (reader->name() == QStringLiteral("node") || reader->name() == QStringLiteral("viewer")) { + Node* node; + + if (reader->name() == QStringLiteral("node")) { + node = XMLLoadNode(reader); + } else { + node = viewer_output_; + } + + if (node) { + node->Load(reader, output_ptrs, desired_connections, footage_connections, cancelled); + + AddNode(node); + } + } else { + reader->skipCurrentElement(); } } // Make connections - foreach (const NodeParam::SerializedConnection& con, desired_connections) { - NodeParam::ConnectEdge(output_ptrs.value(con.output), - con.input); - } + XMLConnectNodes(output_ptrs, desired_connections); // Ensure this and all children are in the main thread // (FIXME: Weird place for this? This should probably be in ProjectLoadManager somehow) @@ -228,7 +199,7 @@ QString Sequence::duration() int64_t timestamp = Timecode::time_to_timestamp(timeline_length, video_params().time_base()); - return Timecode::timestamp_to_timecode(timestamp, video_params().time_base(), Timecode::CurrentDisplay()); + return Timecode::timestamp_to_timecode(timestamp, video_params().time_base(), Core::instance()->GetTimecodeDisplay()); } QString Sequence::rate() diff --git a/app/project/item/sequence/sequence.h b/app/project/item/sequence/sequence.h index c9311cd40..dd91dc2f4 100644 --- a/app/project/item/sequence/sequence.h +++ b/app/project/item/sequence/sequence.h @@ -27,6 +27,7 @@ #include "render/videoparams.h" #include "project/item/footage/stream.h" #include "project/item/item.h" +#include "timeline/timelinepoints.h" class Sequence; using SequencePtr = std::shared_ptr; @@ -34,7 +35,7 @@ using SequencePtr = std::shared_ptr; /** * @brief The main timeline object, an graph of edited clips that forms a complete edit */ -class Sequence : public Item, public NodeGraph +class Sequence : public Item, public NodeGraph, public TimelinePoints { public: Sequence(); diff --git a/app/project/project.cpp b/app/project/project.cpp index 4cb69bd59..7f98197b9 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -23,7 +23,7 @@ #include #include -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "core.h" #include "dialog/progress/progress.h" #include "window/mainwindow/mainwindow.h" @@ -35,29 +35,32 @@ Project::Project() void Project::Load(QXmlStreamReader *reader, const QAtomicInt* cancelled) { + qDebug() << "Hello?"; + QHash footage_ptrs; QList footage_connections; - XMLReadLoop(reader, "project") { - if (reader->isStartElement()) { - if (reader->name() == "folder") { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("folder")) { - // Assume this folder is our root - root_.Load(reader, footage_ptrs, footage_connections, cancelled); + // Assume this folder is our root + root_.Load(reader, footage_ptrs, footage_connections, cancelled); - } else if (reader->name() == "colormanagement") { + } else if (reader->name() == QStringLiteral("colormanagement")) { - // Read color management info - XMLReadLoop(reader, "colormanagement") { - if (reader->name() == "config") { - reader->readNext(); - set_ocio_config(reader->text().toString()); - } else if (reader->name() == "default") { - reader->readNext(); - set_default_input_colorspace(reader->text().toString()); - } + // Read color management info + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("config")) { + set_ocio_config(reader->readElementText()); + } else if (reader->name() == QStringLiteral("default")) { + set_default_input_colorspace(reader->readElementText()); + } else { + reader->skipCurrentElement(); } } + + } else { + reader->skipCurrentElement(); } } @@ -97,7 +100,7 @@ QString Project::name() const if (filename_.isEmpty()) { return tr("(untitled)"); } else { - return QFileInfo(filename_).baseName(); + return QFileInfo(filename_).completeBaseName(); } } @@ -137,3 +140,8 @@ ColorManager *Project::color_manager() { return &color_manager_; } + +QList Project::get_items_of_type(Item::Type type) const +{ + return root_.get_children_of_type(type, true); +} diff --git a/app/project/project.h b/app/project/project.h index b7ee369e1..3a90bb6f3 100644 --- a/app/project/project.h +++ b/app/project/project.h @@ -63,6 +63,8 @@ public: ColorManager* color_manager(); + QList get_items_of_type(Item::Type type) const; + signals: void NameChanged(); diff --git a/app/project/projectloadmanager.cpp b/app/project/projectloadmanager.cpp index a0062f61a..9dd6bbbfe 100644 --- a/app/project/projectloadmanager.cpp +++ b/app/project/projectloadmanager.cpp @@ -4,6 +4,8 @@ #include #include +#include "common/xmlutils.h" + ProjectLoadManager::ProjectLoadManager(const QString &filename) : filename_(filename) { @@ -17,28 +19,32 @@ void ProjectLoadManager::Action() if (project_file.open(QFile::ReadOnly | QFile::Text)) { QXmlStreamReader reader(&project_file); - while (!reader.atEnd()) { - reader.readNext(); + qDebug() << "Hello?"; - if (reader.isStartElement()) { - if (reader.name() == "version") { - reader.readNext(); + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("olive")) { + while(XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("version")) { + qDebug() << "Project version:" << reader.readElementText(); + } else if (reader.name() == QStringLiteral("project")) { + ProjectPtr project = std::make_shared(); - qDebug() << "Project version:" << reader.text(); - } else if (reader.name() == "project") { - ProjectPtr project = std::make_shared(); + project->set_filename(filename_); - project->set_filename(filename_); + project->Load(&reader, &IsCancelled()); - project->Load(&reader, &IsCancelled()); + // Ensure project is in main thread + moveToThread(qApp->thread()); - // Ensure project is in main thread - moveToThread(qApp->thread()); - - if (!IsCancelled()) { - emit ProjectLoaded(project); + if (!IsCancelled()) { + emit ProjectLoaded(project); + } + } else { + reader.skipCurrentElement(); } } + } else { + reader.skipCurrentElement(); } } diff --git a/app/render/audioparams.cpp b/app/render/audioparams.cpp index f91848e55..099c9ceac 100644 --- a/app/render/audioparams.cpp +++ b/app/render/audioparams.cpp @@ -26,6 +26,11 @@ const uint64_t &AudioParams::channel_layout() const return channel_layout_; } +rational AudioParams::time_base() const +{ + return rational(1, sample_rate()); +} + AudioRenderingParams::AudioRenderingParams() : format_(SampleFormat::SAMPLE_FMT_INVALID) { diff --git a/app/render/audioparams.h b/app/render/audioparams.h index 566309c87..308f68fae 100644 --- a/app/render/audioparams.h +++ b/app/render/audioparams.h @@ -14,6 +14,7 @@ public: const int& sample_rate() const; const uint64_t& channel_layout() const; + rational time_base() const; private: int sample_rate_; diff --git a/app/render/backend/audiorenderbackend.cpp b/app/render/backend/audiorenderbackend.cpp index 5baf68c17..70dec1bb2 100644 --- a/app/render/backend/audiorenderbackend.cpp +++ b/app/render/backend/audiorenderbackend.cpp @@ -27,6 +27,8 @@ void AudioRenderBackend::SetParameters(const AudioRenderingParams ¶ms) // Regenerate the cache ID RegenerateCacheID(); + + emit ParamsChanged(); } void AudioRenderBackend::ConnectViewer(ViewerOutput *node) @@ -98,6 +100,18 @@ void AudioRenderBackend::ConnectWorkerToThis(RenderWorker *worker) connect(arw, &AudioRenderWorker::ConformUnavailable, this, &AudioRenderBackend::ConformUnavailable, Qt::QueuedConnection); } +TimeRange AudioRenderBackend::PopNextFrameFromQueue() +{ + TimeRange range = cache_queue_.first(); + + // Limit range per worker to 2 seconds (FIXME: arbitrary, should be tweaked, maybe even in config?) + range.set_out(qMin(range.out(), range.in() + rational(2))); + + cache_queue_.RemoveTimeRange(range); + + return range; +} + void AudioRenderBackend::ConformUnavailable(StreamPtr stream, const TimeRange &range, const rational &stream_time, const AudioRenderingParams& params) { ConformWaitInfo info = {stream, params, range, stream_time}; diff --git a/app/render/backend/audiorenderbackend.h b/app/render/backend/audiorenderbackend.h index 775d7991a..5724e4e16 100644 --- a/app/render/backend/audiorenderbackend.h +++ b/app/render/backend/audiorenderbackend.h @@ -24,6 +24,9 @@ public: QString CachePathName(); +signals: + void ParamsChanged(); + protected: virtual void ConnectViewer(ViewerOutput* node) override; @@ -44,6 +47,8 @@ protected: virtual void ConnectWorkerToThis(RenderWorker* worker) override; + virtual TimeRange PopNextFrameFromQueue() override; + QHash copy_map_; private: diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index 2d2170da3..d486fbcd8 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -397,6 +397,10 @@ void OpenGLProxy::TextureToBuffer(const QVariant &tex_in, void *buffer) { OpenGLTextureCache::ReferencePtr texture = tex_in.value(); + if (!texture) { + return; + } + QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); buffer_.Attach(texture->texture()); buffer_.Bind(); diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index 63aa3d308..07512a4ca 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -87,95 +87,41 @@ bool RenderWorker::IsStarted() return started_; } -NodeValueTable RenderWorker::ProcessNode(const NodeDependency& dep) -{ - const Node* node = dep.node(); - - if (node->IsTrack()) { - // If the range is not wholly contained in this Block, we'll need to do some extra processing - return RenderBlock(static_cast(node), dep.range()); - } - - // FIXME: Cache certain values here if we've already processed them before - - // Generate database of input values of node - NodeValueDatabase database = GenerateDatabase(node, dep.range()); - - // By this point, the node should have all the inputs it needs to render correctly - NodeValueTable table = node->Value(database); - - // Check if we have a shader for this output - RunNodeAccelerated(node, dep.range(), database, &table); - - return table; -} - -NodeValueTable RenderWorker::ProcessInput(const NodeInput *input, const TimeRange& range) -{ - if (input->IsConnected()) { - // Value will equal something from the connected node, follow it - return ProcessNode(NodeDependency(input->get_connected_node(), range)); - } else { - // Push onto the table the value at this time from the input - QVariant input_value = input->get_value_at_time(range.in()); - - NodeValueTable table; - table.Push(input->data_type(), input_value); - return table; - } -} - void RenderWorker::ReportUnavailableFootage(StreamPtr stream, Decoder::RetrieveState state, const rational& stream_time) { emit FootageUnavailable(stream, state, path_.range(), stream_time); } +void RenderWorker::InputProcessingEvent(NodeInput* input, const TimeRange& input_time, NodeValueTable *table) +{ + // Exception for Footage types where we actually retrieve some Footage data from a decoder + if (input->data_type() == NodeParam::kFootage) { + StreamPtr stream = ResolveStreamFromInput(input); + + if (stream) { + DecoderPtr decoder = ResolveDecoderFromInput(stream); + + if (decoder) { + + Decoder::RetrieveState state = decoder->GetRetrieveState(input_time.out()); + + if (state == Decoder::kReady) { + FrameToValue(decoder, stream, input_time, table); + } else { + ReportUnavailableFootage(stream, state, input_time.out()); + } + } + } + } +} + +void RenderWorker::ProcessNodeEvent(const Node *node, const TimeRange &range, const NodeValueDatabase &input_params, NodeValueTable* output_params) +{ + // Check if we have a shader for this output + RunNodeAccelerated(node, range, input_params, output_params); +} + const NodeDependency &RenderWorker::CurrentPath() const { return path_; } - - -#include "common/functiontimer.h" -NodeValueDatabase RenderWorker::GenerateDatabase(const Node* node, const TimeRange &range) -{ - NodeValueDatabase database; - - // We need to insert tables into the database for each input - foreach (NodeParam* param, node->parameters()) { - if (IsCancelled()) { - return NodeValueDatabase(); - } - - if (param->type() == NodeParam::kInput) { - NodeInput* input = static_cast(param); - TimeRange input_time = node->InputTimeAdjustment(input, range); - - NodeValueTable table = ProcessInput(input, input_time); - - // Exception for Footage types where we actually retrieve some Footage data from a decoder - if (input->data_type() == NodeParam::kFootage) { - StreamPtr stream = ResolveStreamFromInput(input); - - if (stream) { - DecoderPtr decoder = ResolveDecoderFromInput(stream); - - if (decoder) { - - Decoder::RetrieveState state = decoder->GetRetrieveState(input_time.out()); - - if (state == Decoder::kReady) { - FrameToValue(decoder, stream, input_time, &table); - } else { - ReportUnavailableFootage(stream, state, input_time.out()); - } - } - } - } - - database.Insert(input, table); - } - } - - return database; -} diff --git a/app/render/backend/renderworker.h b/app/render/backend/renderworker.h index 7965e297e..885d3c60f 100644 --- a/app/render/backend/renderworker.h +++ b/app/render/backend/renderworker.h @@ -3,13 +3,13 @@ #include -#include "common/cancelableobject.h" #include "common/constructors.h" -#include "node/output/track/track.h" -#include "node/node.h" #include "decodercache.h" +#include "node/node.h" +#include "node/output/track/track.h" +#include "node/traverser.h" -class RenderWorker : public QObject, public CancelableObject +class RenderWorker : public QObject, public NodeTraverser { Q_OBJECT public: @@ -38,24 +38,21 @@ protected: virtual void RunNodeAccelerated(const Node *node, const TimeRange& range, const NodeValueDatabase &input_params, NodeValueTable* output_params); - StreamPtr ResolveStreamFromInput(NodeInput* input); - DecoderPtr ResolveDecoderFromInput(StreamPtr stream); - virtual void FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range, NodeValueTable* table) = 0; - NodeValueTable ProcessNode(const NodeDependency &dep); - - virtual NodeValueTable RenderBlock(const TrackOutput *track, const TimeRange& range) = 0; - - NodeValueTable ProcessInput(const NodeInput* input, const TimeRange &range); - virtual void ReportUnavailableFootage(StreamPtr stream, Decoder::RetrieveState state, const rational& stream_time); + virtual void InputProcessingEvent(NodeInput *input, const TimeRange &input_time, NodeValueTable* table) override; + + virtual void ProcessNodeEvent(const Node *node, const TimeRange &range, const NodeValueDatabase &input_params, NodeValueTable* output_params) override; + + StreamPtr ResolveStreamFromInput(NodeInput* input); + + DecoderPtr ResolveDecoderFromInput(StreamPtr stream); + const NodeDependency& CurrentPath() const; private: - NodeValueDatabase GenerateDatabase(const Node *node, const TimeRange &range); - bool started_; DecoderCache* decoder_cache_; diff --git a/app/render/backend/videorenderworker.cpp b/app/render/backend/videorenderworker.cpp index 4c4221426..a2de88f3b 100644 --- a/app/render/backend/videorenderworker.cpp +++ b/app/render/backend/videorenderworker.cpp @@ -179,6 +179,8 @@ void VideoRenderWorker::HashNodeRecursively(QCryptographicHash *hash, const Node 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) { diff --git a/app/shaders/stroke.frag b/app/shaders/stroke.frag index 20ff1607f..7388f71b1 100644 --- a/app/shaders/stroke.frag +++ b/app/shaders/stroke.frag @@ -10,11 +10,18 @@ uniform sampler2D tex_in; uniform vec3 color_in; uniform float radius_in; uniform float opacity_in; +uniform bool inner_in; void main(void) { - if (radius_in == 0.0 || opacity_in == 0.0) { + vec4 pixel_here = texture2D(tex_in, ove_texcoord); + + // Detect no-op situations + if (radius_in == 0.0 + || opacity_in == 0.0 + || (inner_in && pixel_here.a == 0.0) + || (!inner_in && pixel_here.a == 1.0)) { // No-op, do nothing - gl_FragColor = texture2D(tex_in, ove_texcoord); + gl_FragColor = pixel_here; return; } @@ -31,7 +38,13 @@ void main(void) { if (abs(length(vec2(i, j))) < radius) { // Get pixel here - stroke_weight += texture2D(tex_in, ove_texcoord + vec2(x_coord, y_coord)).a; + float alpha = texture2D(tex_in, ove_texcoord + vec2(x_coord, y_coord)).a; + + if (inner_in) { + alpha = 1.0 - alpha; + } + + stroke_weight += alpha; if (stroke_weight >= 1.0) { break; @@ -47,15 +60,21 @@ void main(void) { stroke_weight *= opacity_in * 0.01; + if (inner_in) { + stroke_weight *= pixel_here.a; + } + // Make RGBA color vec4 stroke_col = vec4(vec3(1.0) * stroke_weight, stroke_weight); //vec4 stroke_col = vec4(color_in * stroke_weight, stroke_weight); - // Alpha over color here - vec4 pixel_here = texture2D(tex_in, ove_texcoord); - - stroke_col *= 1.0 - pixel_here.a; - stroke_col += pixel_here; + if (inner_in) { + // Alpha over the stroke over the texture + stroke_col = pixel_here * (1.0 - stroke_col.a) + stroke_col; + } else { + // Alpha over the texture over the stroke + stroke_col = stroke_col * (1.0 - pixel_here.a) + pixel_here; + } gl_FragColor = stroke_col; } diff --git a/app/shaders/stroke.xml b/app/shaders/stroke.xml index 04de10d07..bb9c7aebc 100644 --- a/app/shaders/stroke.xml +++ b/app/shaders/stroke.xml @@ -38,6 +38,12 @@ 100 + + + Inner + false + + diff --git a/app/timeline/CMakeLists.txt b/app/timeline/CMakeLists.txt index c0bd69c9f..000775dcc 100644 --- a/app/timeline/CMakeLists.txt +++ b/app/timeline/CMakeLists.txt @@ -18,6 +18,12 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} timeline/timelinecoordinate.h timeline/timelinecoordinate.cpp + timeline/timelinemarker.h + timeline/timelinemarker.cpp + timeline/timelinepoints.h + timeline/timelinepoints.cpp + timeline/timelineworkarea.h + timeline/timelineworkarea.cpp timeline/trackreference.h timeline/trackreference.cpp PARENT_SCOPE diff --git a/app/timeline/timelinemarker.cpp b/app/timeline/timelinemarker.cpp new file mode 100644 index 000000000..83a136c7b --- /dev/null +++ b/app/timeline/timelinemarker.cpp @@ -0,0 +1,61 @@ +#include "timelinemarker.h" + +TimelineMarker::TimelineMarker(const TimeRange &time, const QString &name, QObject *parent) : + QObject(parent), + time_(time), + name_(name) +{ +} + +const TimeRange &TimelineMarker::time() const +{ + return time_; +} + +void TimelineMarker::set_time(const TimeRange &time) +{ + time_ = time; + emit TimeChanged(time_); +} + +const QString &TimelineMarker::name() const +{ + return name_; +} + +void TimelineMarker::set_name(const QString &name) +{ + name_ = name; + emit NameChanged(name_); +} + +TimelineMarkerList::~TimelineMarkerList() +{ + qDeleteAll(markers_); +} + +void TimelineMarkerList::AddMarker(const TimeRange &time, const QString &name) +{ + TimelineMarker* m = new TimelineMarker(time, name); + markers_.append(m); + emit MarkerAdded(m); +} + +void TimelineMarkerList::RemoveMarker(TimelineMarker *marker) +{ + for (int i=0;i &TimelineMarkerList::list() const +{ + return markers_; +} diff --git a/app/timeline/timelinemarker.h b/app/timeline/timelinemarker.h new file mode 100644 index 000000000..998f27585 --- /dev/null +++ b/app/timeline/timelinemarker.h @@ -0,0 +1,56 @@ +#ifndef TIMELINEMARKER_H +#define TIMELINEMARKER_H + +#include + +#include "common/timerange.h" + +class TimelineMarker : public QObject +{ + Q_OBJECT +public: + TimelineMarker(const TimeRange& time = TimeRange(), const QString& name = QString(), QObject* parent = nullptr); + + const TimeRange &time() const; + void set_time(const TimeRange& time); + + const QString& name() const; + void set_name(const QString& name); + +signals: + void TimeChanged(const TimeRange& time); + + void NameChanged(const QString& name); + +private: + TimeRange time_; + + QString name_; + +}; + +class TimelineMarkerList : public QObject +{ + Q_OBJECT +public: + TimelineMarkerList() = default; + + virtual ~TimelineMarkerList() override; + + void AddMarker(const TimeRange& time = TimeRange(), const QString& name = QString()); + + void RemoveMarker(TimelineMarker* marker); + + const QList &list() const; + +signals: + void MarkerAdded(TimelineMarker* marker); + + void MarkerRemoved(TimelineMarker* marker); + +private: + QList markers_; + +}; + +#endif // TIMELINEMARKER_H diff --git a/app/timeline/timelinepoints.cpp b/app/timeline/timelinepoints.cpp new file mode 100644 index 000000000..b3aae146c --- /dev/null +++ b/app/timeline/timelinepoints.cpp @@ -0,0 +1,21 @@ +#include "timelinepoints.h" + +TimelineMarkerList *TimelinePoints::markers() +{ + return &markers_; +} + +const TimelineMarkerList *TimelinePoints::markers() const +{ + return &markers_; +} + +const TimelineWorkArea *TimelinePoints::workarea() const +{ + return &workarea_; +} + +TimelineWorkArea *TimelinePoints::workarea() +{ + return &workarea_; +} diff --git a/app/timeline/timelinepoints.h b/app/timeline/timelinepoints.h new file mode 100644 index 000000000..ed2570f1c --- /dev/null +++ b/app/timeline/timelinepoints.h @@ -0,0 +1,25 @@ +#ifndef TIMELINEPOINTS_H +#define TIMELINEPOINTS_H + +#include "timelinemarker.h" +#include "timelineworkarea.h" + +class TimelinePoints +{ +public: + TimelinePoints() = default; + + TimelineMarkerList* markers(); + const TimelineMarkerList* markers() const; + + TimelineWorkArea* workarea(); + const TimelineWorkArea* workarea() const; + +private: + TimelineMarkerList markers_; + + TimelineWorkArea workarea_; + +}; + +#endif // TIMELINEPOINTS_H diff --git a/app/timeline/timelineworkarea.cpp b/app/timeline/timelineworkarea.cpp new file mode 100644 index 000000000..57b29364a --- /dev/null +++ b/app/timeline/timelineworkarea.cpp @@ -0,0 +1,41 @@ +#include "timelineworkarea.h" + +const rational TimelineWorkArea::kResetIn = 0; +const rational TimelineWorkArea::kResetOut = RATIONAL_MAX; + +TimelineWorkArea::TimelineWorkArea(QObject *parent) : + QObject(parent) +{ +} + +bool TimelineWorkArea::enabled() const +{ + return workarea_enabled_; +} + +void TimelineWorkArea::set_enabled(bool e) +{ + workarea_enabled_ = e; + emit EnabledChanged(workarea_enabled_); +} + +const TimeRange &TimelineWorkArea::range() const +{ + return workarea_range_; +} + +void TimelineWorkArea::set_range(const TimeRange &range) +{ + workarea_range_ = range; + emit RangeChanged(workarea_range_); +} + +const rational &TimelineWorkArea::in() const +{ + return workarea_range_.in(); +} + +const rational &TimelineWorkArea::out() const +{ + return workarea_range_.out(); +} diff --git a/app/timeline/timelineworkarea.h b/app/timeline/timelineworkarea.h new file mode 100644 index 000000000..bad23bc82 --- /dev/null +++ b/app/timeline/timelineworkarea.h @@ -0,0 +1,37 @@ +#ifndef TIMELINEWORKAREA_H +#define TIMELINEWORKAREA_H + +#include + +#include "common/timerange.h" + +class TimelineWorkArea : public QObject +{ + Q_OBJECT +public: + TimelineWorkArea(QObject* parent = nullptr); + + bool enabled() const; + void set_enabled(bool e); + + const rational& in() const; + const rational& out() const; + const TimeRange& range() const; + void set_range(const TimeRange& range); + + static const rational kResetIn; + static const rational kResetOut; + +signals: + void EnabledChanged(bool e); + + void RangeChanged(const TimeRange& r); + +private: + bool workarea_enabled_; + + TimeRange workarea_range_; + +}; + +#endif // TIMELINEWORKAREA_H diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index 6cd1df5f4..fb36b2238 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -34,25 +34,25 @@ MenuShared::MenuShared() new_folder_item_ = Menu::CreateItem(this, "newfolder", Core::instance(), SLOT(CreateNewFolder())); // "Edit" menu shared items - edit_cut_item_ = Menu::CreateItem(this, "cut", nullptr, nullptr, "Ctrl+X"); - edit_copy_item_ = Menu::CreateItem(this, "copy", nullptr, nullptr, "Ctrl+C"); - edit_paste_item_ = Menu::CreateItem(this, "paste", nullptr, nullptr, "Ctrl+V"); - edit_paste_insert_item_ = Menu::CreateItem(this, "pasteinsert", nullptr, nullptr, "Ctrl+Shift+V"); + edit_cut_item_ = Menu::CreateItem(this, "cut", this, SLOT(CutTriggered()), "Ctrl+X"); + edit_copy_item_ = Menu::CreateItem(this, "copy", this, SLOT(CopyTriggered()), "Ctrl+C"); + edit_paste_item_ = Menu::CreateItem(this, "paste", this, SLOT(PasteTriggered()), "Ctrl+V"); + edit_paste_insert_item_ = Menu::CreateItem(this, "pasteinsert", this, SLOT(PasteInsertTriggered()), "Ctrl+Shift+V"); edit_duplicate_item_ = Menu::CreateItem(this, "duplicate", nullptr, nullptr, "Ctrl+D"); - edit_delete_item_ = Menu::CreateItem(this, "delete", this, SLOT(DeleteSelected()), "Del"); - edit_ripple_delete_item_ = Menu::CreateItem(this, "rippledelete", this, SLOT(RippleDelete()), "Shift+Del"); - edit_split_item_ = Menu::CreateItem(this, "split", this, SLOT(SplitAtPlayhead()), "Ctrl+K"); + edit_delete_item_ = Menu::CreateItem(this, "delete", this, SLOT(DeleteSelectedTriggered()), "Del"); + edit_ripple_delete_item_ = Menu::CreateItem(this, "rippledelete", this, SLOT(RippleDeleteTriggered()), "Shift+Del"); + edit_split_item_ = Menu::CreateItem(this, "split", this, SLOT(SplitAtPlayheadTriggered()), "Ctrl+K"); // "In/Out" menu shared items - inout_set_in_item_ = Menu::CreateItem(this, "setinpoint", nullptr, nullptr, "I"); - inout_set_out_item_ = Menu::CreateItem(this, "setoutpoint", nullptr, nullptr, "O"); - inout_reset_in_item_ = Menu::CreateItem(this, "resetin", nullptr, nullptr); - inout_reset_out_item_ = Menu::CreateItem(this, "resetout", nullptr, nullptr); - inout_clear_inout_item_ = Menu::CreateItem(this, "clearinout", nullptr, nullptr, "G"); + inout_set_in_item_ = Menu::CreateItem(this, "setinpoint", this, SLOT(SetInTriggered()), "I"); + inout_set_out_item_ = Menu::CreateItem(this, "setoutpoint", this, SLOT(SetOutTriggered()), "O"); + inout_reset_in_item_ = Menu::CreateItem(this, "resetin", this, SLOT(ResetInTriggered())); + inout_reset_out_item_ = Menu::CreateItem(this, "resetout", this, SLOT(ResetOutTriggered())); + inout_clear_inout_item_ = Menu::CreateItem(this, "clearinout", this, SLOT(ClearInOutTriggered()), "G"); // "Clip Edit" menu shared items clip_add_default_transition_item_ = Menu::CreateItem(this, "deftransition", nullptr, nullptr, "Ctrl+Shift+D"); - clip_link_unlink_item_ = Menu::CreateItem(this, "linkunlink", nullptr, nullptr, "Ctrl+L"); + clip_link_unlink_item_ = Menu::CreateItem(this, "linkunlink", this, SLOT(ToggleLinksTriggered()), "Ctrl+L"); clip_enable_disable_item_ = Menu::CreateItem(this, "enabledisable", nullptr, nullptr, "Shift+E"); clip_nest_item_ = Menu::CreateItem(this, "nest", nullptr, nullptr); @@ -112,7 +112,7 @@ MenuShared *MenuShared::instance() return instance_; } -void MenuShared::SplitAtPlayhead() +void MenuShared::SplitAtPlayheadTriggered() { TimelinePanel* timeline = PanelManager::instance()->MostRecentlyFocused(); @@ -121,16 +121,66 @@ void MenuShared::SplitAtPlayhead() } } -void MenuShared::DeleteSelected() +void MenuShared::DeleteSelectedTriggered() { PanelManager::instance()->CurrentlyFocused()->DeleteSelected(); } -void MenuShared::RippleDelete() +void MenuShared::RippleDeleteTriggered() { PanelManager::instance()->CurrentlyFocused()->RippleDelete(); } +void MenuShared::SetInTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->SetIn(); +} + +void MenuShared::SetOutTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->SetOut(); +} + +void MenuShared::ResetInTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->ResetIn(); +} + +void MenuShared::ResetOutTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->ResetOut(); +} + +void MenuShared::ClearInOutTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->ClearInOut(); +} + +void MenuShared::ToggleLinksTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->ToggleLinks(); +} + +void MenuShared::CutTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->CutSelected(); +} + +void MenuShared::CopyTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->CopySelected(); +} + +void MenuShared::PasteTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->Paste(); +} + +void MenuShared::PasteInsertTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->PasteInsert(); +} + void MenuShared::Retranslate() { // "New" menu shared items diff --git a/app/widget/menu/menushared.h b/app/widget/menu/menushared.h index 2cee72066..c1e6e54ff 100644 --- a/app/widget/menu/menushared.h +++ b/app/widget/menu/menushared.h @@ -75,11 +75,31 @@ private: static MenuShared* instance_; private slots: - void SplitAtPlayhead(); + void SplitAtPlayheadTriggered(); - void DeleteSelected(); + void DeleteSelectedTriggered(); - void RippleDelete(); + void RippleDeleteTriggered(); + + void SetInTriggered(); + + void SetOutTriggered(); + + void ResetInTriggered(); + + void ResetOutTriggered(); + + void ClearInOutTriggered(); + + void ToggleLinksTriggered(); + + void CutTriggered(); + + void CopyTriggered(); + + void PasteTriggered(); + + void PasteInsertTriggered(); }; diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index f1d895a05..9a1ce07cc 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -20,11 +20,14 @@ #include "nodeview.h" +#include #include +#include #include "core.h" #include "nodeviewundo.h" #include "node/factory.h" +#include "common/xmlutils.h" NodeView::NodeView(QWidget *parent) : QGraphicsView(parent), @@ -90,16 +93,7 @@ void NodeView::DeleteSelected() return; } - QList selected = scene_.selectedItems(); - QList selected_nodes; - - foreach (QGraphicsItem* item, selected) { - NodeViewItem* node_item = dynamic_cast(item); - - if (node_item) { - selected_nodes.append(node_item->node()); - } - } + QList selected_nodes = scene_.GetSelectedNodes(); if (selected_nodes.isEmpty()) { return; @@ -110,20 +104,12 @@ void NodeView::DeleteSelected() void NodeView::SelectAll() { - QList all_items = this->items(); - - foreach (QGraphicsItem* i, all_items) { - i->setSelected(true); - } + scene_.SelectAll(); } void NodeView::DeselectAll() { - QList selected_items = scene_.selectedItems(); - - foreach (QGraphicsItem* i, selected_items) { - i->setSelected(false); - } + scene_.DeselectAll(); } void NodeView::Select(const QList &nodes) @@ -133,9 +119,6 @@ void NodeView::Select(const QList &nodes) foreach (Node* n, nodes) { NodeViewItem* item = scene_.NodeToUIObject(n); - Q_ASSERT(n); - Q_ASSERT(item); - item->setSelected(true); } } @@ -150,6 +133,125 @@ void NodeView::SelectWithDependencies(QList nodes) Select(nodes); } +void NodeView::CopySelected(bool cut) +{ + if (!graph_) { + return; + } + + QList selected = scene_.GetSelectedNodes(); + + if (selected.isEmpty()) { + return; + } + + QString copy_str; + + QXmlStreamWriter writer(©_str); + writer.setAutoFormatting(true); + + writer.writeStartDocument(); + writer.writeStartElement(QStringLiteral("olive")); + + foreach (Node* n, selected) { + n->Save(&writer); + } + + writer.writeEndElement(); // clipboard + writer.writeEndDocument(); + + if (cut) { + DeleteSelected(); + } + + QGuiApplication::clipboard()->setText(copy_str); +} + +void NodeView::Paste() +{ + if (!graph_) { + return; + } + + QString clipboard = QGuiApplication::clipboard()->text(); + + if (clipboard.isEmpty()) { + return; + } + + QXmlStreamReader reader(clipboard); + + QList pasted_nodes; + QHash output_ptrs; + QList desired_connections; + QList footage_connections; + + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("olive")) { + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("node")) { + Node* node = XMLLoadNode(&reader); + + if (node) { + node->Load(&reader, output_ptrs, desired_connections, footage_connections, nullptr); + + graph_->AddNode(node); + + pasted_nodes.append(node); + } + } else { + reader.skipCurrentElement(); + } + } + } else { + reader.skipCurrentElement(); + } + } + + // Make connections + if (!desired_connections.isEmpty()) { + XMLConnectNodes(output_ptrs, desired_connections); + } + + // Connect footage to existing footage if it exists + if (!footage_connections.isEmpty()) { + // Get list of all footage from project + // FIXME: Assumes sequence + QList footage = static_cast(graph_)->project()->get_items_of_type(Item::kFootage); + + if (!footage.isEmpty()) { + foreach (const NodeInput::FootageConnection& con, footage_connections) { + if (con.footage) { + // Assume this is a pointer to a Stream* + Stream* loaded_stream = reinterpret_cast(con.footage); + + bool found = false; + + foreach (ItemPtr item, footage) { + const QList& streams = std::static_pointer_cast(item)->streams(); + + foreach (StreamPtr s, streams) { + if (s.get() == loaded_stream) { + con.input->set_standard_value(QVariant::fromValue(s)); + found = true; + break; + } + } + + if (found) { + break; + } + } + } + } + } + } + + if (!pasted_nodes.isEmpty()) { + // FIXME: Attach to cursor so user can drop in place + } +} + void NodeView::ItemsChanged() { QHash::const_iterator i; @@ -263,21 +365,7 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) void NodeView::SceneSelectionChangedSlot() { - // Get the scene's selected items and convert it into a list of selected nodes - QList selected_items = scene_.selectedItems(); - - QList selected_nodes; - - for (int i=0;i(selected_items.at(i)); - - if (item != nullptr) { - selected_nodes.append(item->node()); - } - } - - emit SelectionChanged(selected_nodes); + emit SelectionChanged(scene_.GetSelectedNodes()); } void NodeView::ShowContextMenu(const QPoint &pos) diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 4ea98bea3..7811ae4bb 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -57,6 +57,9 @@ public: void Select(const QList& nodes); void SelectWithDependencies(QList nodes); + void CopySelected(bool cut); + void Paste(); + signals: /** * @brief Signal emitted when the selected nodes have changed diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 750e391a2..dffd8702f 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -10,6 +10,15 @@ NodeViewScene::NodeViewScene(QObject *parent) : 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 + // 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. + selectedItems(); + { QHash::const_iterator i; for (i=item_map_.begin();i!=item_map_.end();i++) { @@ -27,6 +36,24 @@ void NodeViewScene::clear() } } +void NodeViewScene::SelectAll() +{ + QList all_items = this->items(); + + foreach (QGraphicsItem* i, all_items) { + i->setSelected(true); + } +} + +void NodeViewScene::DeselectAll() +{ + QList selected_items = this->selectedItems(); + + foreach (QGraphicsItem* i, selected_items) { + i->setSelected(false); + } +} + NodeViewItem *NodeViewScene::NodeToUIObject(Node *n) { return item_map_.value(n); @@ -42,6 +69,34 @@ void NodeViewScene::SetGraph(NodeGraph *graph) graph_ = graph; } +QList NodeViewScene::GetSelectedNodes() const +{ + QHash::const_iterator iterator; + QList selected; + + for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) { + if (iterator.value()->isSelected()) { + selected.append(iterator.key()); + } + } + + return selected; +} + +QList NodeViewScene::GetSelectedItems() const +{ + QHash::const_iterator iterator; + QList selected; + + for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) { + if (iterator.value()->isSelected()) { + selected.append(iterator.value()); + } + } + + return selected; +} + const QHash &NodeViewScene::item_map() const { return item_map_; diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 2c768fd11..8a2c22933 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -16,6 +16,9 @@ public: void clear(); + void SelectAll(); + void DeselectAll(); + /** * @brief Retrieve the graphical widget corresponding to a specific Node * @@ -38,6 +41,9 @@ public: void SetGraph(NodeGraph* graph); + QList GetSelectedNodes() const; + QList GetSelectedItems() const; + const QHash& item_map() const; const QHash& edge_map() const; diff --git a/app/widget/panel/panel.h b/app/widget/panel/panel.h index e324c149f..ff8ce54b1 100644 --- a/app/widget/panel/panel.h +++ b/app/widget/panel/panel.h @@ -118,6 +118,28 @@ public: virtual void DecreaseTrackHeight(){} + virtual void SetIn(){} + + virtual void SetOut(){} + + virtual void ResetIn(){} + + virtual void ResetOut(){} + + virtual void ClearInOut(){} + + virtual void SetMarker(){} + + virtual void ToggleLinks(){} + + virtual void CutSelected(){} + + virtual void CopySelected(){} + + virtual void Paste(){} + + virtual void PasteInsert(){} + protected: /** * @brief paintEvent diff --git a/app/widget/playbackcontrols/playbackcontrols.cpp b/app/widget/playbackcontrols/playbackcontrols.cpp index 523a7a9e5..400fdedba 100644 --- a/app/widget/playbackcontrols/playbackcontrols.cpp +++ b/app/widget/playbackcontrols/playbackcontrols.cpp @@ -24,7 +24,7 @@ #include #include -#include "common/timecodefunctions.h" +#include "core.h" #include "config/config.h" #include "ui/icons/icons.h" @@ -120,6 +120,8 @@ PlaybackControls::PlaybackControls(QWidget *parent) : UpdateIcons(); SetTimebase(0); + + connect(Core::instance(), &Core::TimecodeDisplayChanged, this, &PlaybackControls::TimecodeChanged); } void PlaybackControls::SetTimecodeEnabled(bool enabled) @@ -147,9 +149,11 @@ void PlaybackControls::SetEndTime(const int64_t &r) return; } - end_tc_lbl_->setText(Timecode::timestamp_to_timecode(r, + end_time_ = r; + + end_tc_lbl_->setText(Timecode::timestamp_to_timecode(end_time_, time_base_, - Timecode::CurrentDisplay())); + Core::instance()->GetTimecodeDisplay())); } void PlaybackControls::ShowPauseButton() @@ -181,3 +185,9 @@ void PlaybackControls::UpdateIcons() next_frame_btn_->setIcon(icon::NextFrame); go_to_end_btn_->setIcon(icon::GoToEnd); } + +void PlaybackControls::TimecodeChanged() +{ + // Update end time + SetEndTime(end_time_); +} diff --git a/app/widget/playbackcontrols/playbackcontrols.h b/app/widget/playbackcontrols/playbackcontrols.h index 2c9701d91..9ad983f0b 100644 --- a/app/widget/playbackcontrols/playbackcontrols.h +++ b/app/widget/playbackcontrols/playbackcontrols.h @@ -101,6 +101,8 @@ private: TimeSlider* cur_tc_lbl_; QLabel* end_tc_lbl_; + int64_t end_time_; + rational time_base_; QPushButton* go_to_start_btn_; @@ -113,7 +115,7 @@ private: QStackedWidget* playpause_stack_; private slots: - + void TimecodeChanged(); }; diff --git a/app/widget/resizablescrollbar/resizablescrollbar.cpp b/app/widget/resizablescrollbar/resizablescrollbar.cpp index fc6a79d44..f18a86037 100644 --- a/app/widget/resizablescrollbar/resizablescrollbar.cpp +++ b/app/widget/resizablescrollbar/resizablescrollbar.cpp @@ -39,11 +39,8 @@ void ResizableScrollBar::mouseMoveEvent(QMouseEvent *event) QRect sr = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); - QRect gr = style()->subControlRect(QStyle::CC_ScrollBar, &opt, - QStyle::SC_ScrollBarGroove, this); if (mouse_dragging_) { - int new_drag_pos = GetActiveMousePos(event); int mouse_movement = new_drag_pos - mouse_drag_start_; mouse_drag_start_ = new_drag_pos; @@ -52,21 +49,29 @@ void ResizableScrollBar::mouseMoveEvent(QMouseEvent *event) mouse_movement = -mouse_movement; } - double scale_multiplier = static_cast(sr.width()) / static_cast(sr.width() + mouse_movement); - emit RequestScale(scale_multiplier); + double width_adjustment = static_cast(sr.width() + mouse_movement); - if (mouse_handle_state_ == kInTopHandle) { - int slider_min = gr.x(); - int slider_max = gr.right() - (sr.width() + mouse_movement); - int val = QStyle::sliderValueFromPosition(minimum(), - maximum(), - event->pos().x() - slider_min, - slider_max - slider_min, - opt.upsideDown); + // Prevent dividing by zero or emitting a negative scale + if (width_adjustment > 0) { + double scale_multiplier = static_cast(sr.width()) / width_adjustment; + emit RequestScale(scale_multiplier); - setValue(val); - } else { - setValue(qRound(static_cast(value()) * scale_multiplier)); + if (mouse_handle_state_ == kInTopHandle) { + QRect gr = style()->subControlRect(QStyle::CC_ScrollBar, &opt, + QStyle::SC_ScrollBarGroove, this); + + int slider_min = gr.x(); + int slider_max = gr.right() - (sr.width() + mouse_movement); + int val = QStyle::sliderValueFromPosition(minimum(), + maximum(), + event->pos().x() - slider_min, + slider_max - slider_min, + opt.upsideDown); + + setValue(val); + } else { + setValue(qRound(static_cast(value()) * scale_multiplier)); + } } } else { diff --git a/app/widget/slider/timeslider.cpp b/app/widget/slider/timeslider.cpp index 5899596db..4d59c007c 100644 --- a/app/widget/slider/timeslider.cpp +++ b/app/widget/slider/timeslider.cpp @@ -1,11 +1,14 @@ #include "timeslider.h" #include "common/timecodefunctions.h" +#include "core.h" TimeSlider::TimeSlider(QWidget *parent) : IntegerSlider(parent) { SetMinimum(0); + + connect(Core::instance(), &Core::TimecodeDisplayChanged, this, &TimeSlider::TimecodeDisplayChanged); } void TimeSlider::SetTimebase(const rational &timebase) @@ -25,10 +28,15 @@ QString TimeSlider::ValueToString(const QVariant &v) return Timecode::timestamp_to_timecode(v.toLongLong(), timebase_, - Timecode::CurrentDisplay()); + Core::instance()->GetTimecodeDisplay()); } QVariant TimeSlider::StringToValue(const QString &s, bool *ok) { - return QVariant::fromValue(Timecode::timecode_to_timestamp(s, timebase_, Timecode::CurrentDisplay(), ok)); + return QVariant::fromValue(Timecode::timecode_to_timestamp(s, timebase_, Core::instance()->GetTimecodeDisplay(), ok)); +} + +void TimeSlider::TimecodeDisplayChanged() +{ + UpdateLabel(Value()); } diff --git a/app/widget/slider/timeslider.h b/app/widget/slider/timeslider.h index 331572d96..a69c0f31b 100644 --- a/app/widget/slider/timeslider.h +++ b/app/widget/slider/timeslider.h @@ -6,6 +6,7 @@ class TimeSlider : public IntegerSlider { + Q_OBJECT public: TimeSlider(QWidget* parent = nullptr); @@ -19,6 +20,9 @@ protected: private: rational timebase_; +private slots: + void TimecodeDisplayChanged(); + }; #endif // TIMESLIDER_H diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index b7feb9bb6..96cd6e025 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -1,11 +1,19 @@ #include "timebased.h" +#include +#include + #include "common/timecodefunctions.h" +#include "config/config.h" +#include "core.h" +#include "project/item/sequence/sequence.h" +#include "widget/timelinewidget/undo/undo.h" TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_status_visible, QWidget *parent) : - QWidget(parent), + TimelineScaledWidget(parent), viewer_node_(nullptr), - auto_max_scrollbar_(false) + auto_max_scrollbar_(false), + points_(nullptr) { ruler_ = new TimeRuler(ruler_text_visible, ruler_cache_status_visible, this); connect(ruler_, &TimeRuler::TimeChanged, this, &TimeBasedWidget::SetTimeAndSignal); @@ -44,6 +52,9 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) DisconnectNodeInternal(viewer_node_); disconnect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); + + points_ = nullptr; + ruler()->ConnectTimelinePoints(nullptr); } viewer_node_ = node; @@ -51,9 +62,13 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) ConnectedNodeChanged(viewer_node_); if (viewer_node_) { - ConnectNodeInternal(viewer_node_); - connect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); + + if ((points_ = ConnectTimelinePoints())) { + ruler()->ConnectTimelinePoints(points_); + } + + ConnectNodeInternal(viewer_node_); } } @@ -68,7 +83,25 @@ void TimeBasedWidget::UpdateMaximumScroll() void TimeBasedWidget::ScrollBarResized(const double &multiplier) { - SetScale(GetScale() * multiplier); + QScrollBar* bar = static_cast(sender()); + + // Our extension area (represented by a TimelineViewEndItem) is NOT scaled, but the ResizableScrollBar doesn't know + // this. Here we re-calculate the requested scale knowing that the end item is not affected by scale. + + int current_max = bar->maximum(); + double proposed_max = static_cast(current_max) * multiplier; + + proposed_max = proposed_max - (bar->width() * 0.5 / multiplier) + (bar->width() * 0.5); + + double corrected_scale; + + if (current_max == 0) { + corrected_scale = multiplier; + } else { + corrected_scale = (proposed_max / static_cast(current_max)); + } + + SetScale(GetScale() * corrected_scale); } TimeRuler *TimeBasedWidget::ruler() const @@ -114,6 +147,16 @@ void TimeBasedWidget::resizeEvent(QResizeEvent *event) UpdateMaximumScroll(); } +TimelinePoints *TimeBasedWidget::ConnectTimelinePoints() +{ + return static_cast(viewer_node_->parent()); +} + +TimelinePoints *TimeBasedWidget::GetConnectedTimelinePoints() const +{ + return points_; +} + void TimeBasedWidget::SetTime(int64_t timestamp) { ruler_->SetTime(timestamp); @@ -128,6 +171,7 @@ void TimeBasedWidget::SetTimebase(const rational &timebase) void TimeBasedWidget::SetScale(const double &scale) { + // Simple QObject slot wrapper around TimelineScaledObject::SetScale() TimelineScaledObject::SetScale(scale); } @@ -242,3 +286,110 @@ void TimeBasedWidget::CenterScrollOnPlayhead() { scrollbar_->setValue(qRound(TimeToScene(Timecode::timestamp_to_time(ruler_->GetTime(), timebase()))) - scrollbar_->width()/2); } + +void TimeBasedWidget::SetPoint(Timeline::MovementMode m, const rational& time) +{ + if (!points_) { + return; + } + + QUndoCommand* command = new QUndoCommand(); + + // Enable workarea if it isn't already enabled + if (!points_->workarea()->enabled()) { + new WorkareaSetEnabledCommand(points_, true, command); + } + + // Determine our new range + rational in_point, out_point; + + if (m == Timeline::kTrimIn) { + in_point = time; + + if (!points_->workarea()->enabled() || points_->workarea()->out() < in_point) { + out_point = TimelineWorkArea::kResetOut; + } else { + out_point = points_->workarea()->out(); + } + } else { + out_point = time; + + if (!points_->workarea()->enabled() || points_->workarea()->in() > out_point) { + in_point = TimelineWorkArea::kResetIn; + } else { + in_point = points_->workarea()->in(); + } + } + + // Set workarea + new WorkareaSetRangeCommand(points_, TimeRange(in_point, out_point), command); + + Core::instance()->undo_stack()->push(command); +} + +void TimeBasedWidget::ResetPoint(Timeline::MovementMode m) +{ + if (!points_ || !points_->workarea()->enabled()) { + return; + } + + TimeRange r = points_->workarea()->range(); + + if (m == Timeline::kTrimIn) { + r.set_in(TimelineWorkArea::kResetIn); + } else { + r.set_out(TimelineWorkArea::kResetOut); + } + + Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(points_, r)); +} + +void TimeBasedWidget::SetInAtPlayhead() +{ + SetPoint(Timeline::kTrimIn, GetTime()); +} + +void TimeBasedWidget::SetOutAtPlayhead() +{ + SetPoint(Timeline::kTrimOut, GetTime()); +} + +void TimeBasedWidget::ResetIn() +{ + ResetPoint(Timeline::kTrimIn); +} + +void TimeBasedWidget::ResetOut() +{ + ResetPoint(Timeline::kTrimOut); +} + +void TimeBasedWidget::ClearInOutPoints() +{ + if (!points_) { + return; + } + + + Core::instance()->undo_stack()->push(new WorkareaSetEnabledCommand(points_, false)); +} + +void TimeBasedWidget::SetMarker() +{ + if (!points_) { + return; + } + + bool ok; + QString marker_name; + + if (Config::Current()["SetNameWithMarker"].toBool()) { + marker_name = QInputDialog::getText(this, tr("Set Marker"), tr("Marker name:"), QLineEdit::Normal, QString(), &ok); + } else { + ok = true; + } + + if (ok) { + points_->markers()->AddMarker(TimeRange(GetTime(), GetTime()), marker_name); + } +} diff --git a/app/widget/timebased/timebased.h b/app/widget/timebased/timebased.h index 50b0f4882..40e8bf513 100644 --- a/app/widget/timebased/timebased.h +++ b/app/widget/timebased/timebased.h @@ -3,12 +3,13 @@ #include +#include "common/timelinecommon.h" #include "node/output/viewer/viewer.h" #include "widget/resizablescrollbar/resizablescrollbar.h" #include "widget/timelinewidget/timelinescaledobject.h" #include "widget/timeruler/timeruler.h" -class TimeBasedWidget : public QWidget, public TimelineScaledObject +class TimeBasedWidget : public TimelineScaledWidget { Q_OBJECT public: @@ -48,6 +49,18 @@ public slots: void GoToNextCut(); + void SetInAtPlayhead(); + + void SetOutAtPlayhead(); + + void ResetIn(); + + void ResetOut(); + + void ClearInOutPoints(); + + void SetMarker(); + TimeRuler* ruler() const; protected slots: @@ -72,6 +85,10 @@ protected: virtual void resizeEvent(QResizeEvent *event) override; + virtual TimelinePoints* ConnectTimelinePoints(); + + TimelinePoints* GetConnectedTimelinePoints() const; + protected slots: /** * @brief Slot to center the horizontal scroll bar on the playhead's current position @@ -84,6 +101,26 @@ signals: void TimebaseChanged(const rational&); private: + /** + * @brief Set either in or out point to the current playhead + * + * @param m + * + * Set to kTrimIn or kTrimOut for setting the in point or out point respectively. + */ + void SetPoint(Timeline::MovementMode m, const rational &time); + + /** + * @brief Reset either the in or out point + * + * Sets either the in point to 0 or the out point to `RATIONAL_MAX`. + * + * @param m + * + * Set to kTrimIn or kTrimOut for setting the in point or out point respectively. + */ + void ResetPoint(Timeline::MovementMode m); + ViewerOutput* viewer_node_; TimeRuler* ruler_; @@ -92,6 +129,8 @@ private: bool auto_max_scrollbar_; + TimelinePoints* points_; + private slots: void UpdateMaximumScroll(); diff --git a/app/widget/timelinewidget/timelinescaledobject.cpp b/app/widget/timelinewidget/timelinescaledobject.cpp index f3a467782..cd0fb9bcc 100644 --- a/app/widget/timelinewidget/timelinescaledobject.cpp +++ b/app/widget/timelinewidget/timelinescaledobject.cpp @@ -3,8 +3,11 @@ #include #include +#include "common/clamp.h" + TimelineScaledObject::TimelineScaledObject() : scale_(1.0), + min_scale_(0), max_scale_(DBL_MAX) { @@ -64,6 +67,15 @@ void TimelineScaledObject::SetMaximumScale(const double &max) } } +void TimelineScaledObject::SetMinimumScale(const double &min) +{ + min_scale_ = min; + + if (GetScale() < min_scale_) { + SetScale(min_scale_); + } +} + const double& TimelineScaledObject::GetScale() const { return scale_; @@ -71,7 +83,14 @@ const double& TimelineScaledObject::GetScale() const void TimelineScaledObject::SetScale(const double& scale) { - scale_ = scale; + Q_ASSERT(scale > 0); + + scale_ = clamp(scale, min_scale_, max_scale_); ScaleChangedEvent(scale_); } + +TimelineScaledWidget::TimelineScaledWidget(QWidget *parent) : + QWidget(parent) +{ +} diff --git a/app/widget/timelinewidget/timelinescaledobject.h b/app/widget/timelinewidget/timelinescaledobject.h index ed6a6bcb7..0889064c8 100644 --- a/app/widget/timelinewidget/timelinescaledobject.h +++ b/app/widget/timelinewidget/timelinescaledobject.h @@ -1,6 +1,8 @@ #ifndef TIMELINESCALEDOBJECT_H #define TIMELINESCALEDOBJECT_H +#include + #include "common/rational.h" class TimelineScaledObject @@ -30,6 +32,8 @@ protected: void SetMaximumScale(const double& max); + void SetMinimumScale(const double& min); + private: rational timebase_; @@ -37,8 +41,17 @@ private: double scale_; + double min_scale_; + double max_scale_; }; +class TimelineScaledWidget : public QWidget, public TimelineScaledObject +{ + Q_OBJECT +public: + TimelineScaledWidget(QWidget* parent = nullptr); +}; + #endif // TIMELINESCALEDOBJECT_H diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 6173d9df2..2a19176a7 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -335,7 +335,7 @@ void TimelineWidget::SplitAtPlayhead() } } -void TimelineWidget::DeleteSelectedInternal(QList blocks, +void TimelineWidget::DeleteSelectedInternal(const QList &blocks, bool transition_aware, bool remove_from_graph, QUndoCommand *command) @@ -380,6 +380,8 @@ void TimelineWidget::DeleteSelectedInternal(QList blocks, } if (remove_from_graph) { + new BlockUnlinkAllCommand(b, command); + new NodeRemoveWithExclusiveDeps(static_cast(b->parent()), b, command); } } @@ -426,7 +428,7 @@ void TimelineWidget::DeleteSelected(bool ripple) range_list.InsertTimeRange(TimeRange(b->in(), b->out())); } - new TimelineRippleDeleteGapsAtRegions(GetConnectedNode(), range_list, command); + new TimelineRippleDeleteGapsAtRegionsCommand(GetConnectedNode(), range_list, command); } Core::instance()->undo_stack()->pushIfHasChildren(command); @@ -470,6 +472,30 @@ void TimelineWidget::OverwriteFootageAtPlayhead(const QList &footage) import_tool_->PlaceAt(footage, GetTime(), false); } +void TimelineWidget::ToggleLinksOnSelected() +{ + QList sel = GetSelectedBlocks(); + + // Prioritize unlinking + + QList blocks; + bool link = true; + + foreach (TimelineViewBlockItem* item, sel) { + if (link && item->block()->HasLinks()) { + link = false; + } + + blocks.append(item->block()); + } + + if (link) { + Core::instance()->undo_stack()->push(new BlockLinkManyCommand(blocks, true)); + } else { + Core::instance()->undo_stack()->push(new BlockLinkManyCommand(blocks, false)); + } +} + QList TimelineWidget::GetSelectedBlocks() { QList list; @@ -685,6 +711,7 @@ void TimelineWidget::AddBlock(Block *block, TrackReference track) views_.at(track.type())->view()->scene()->addItem(item); connect(block, &Block::Refreshed, this, &TimelineWidget::BlockChanged); + connect(block, &Block::LinksChanged, this, &TimelineWidget::PreviewUpdated); if (block->type() == Block::kClip) { connect(static_cast(block), &ClipBlock::PreviewUpdated, this, &TimelineWidget::PreviewUpdated); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 2dd6bd758..86534a45a 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -57,6 +57,8 @@ public: void OverwriteFootageAtPlayhead(const QList &footage); + void ToggleLinksOnSelected(); + QList GetSelectedBlocks(); signals: @@ -349,7 +351,7 @@ private: bool dual_transition_; }; - void DeleteSelectedInternal(QList blocks, bool transition_aware, bool remove_from_graph, QUndoCommand* command); + void DeleteSelectedInternal(const QList& blocks, bool transition_aware, bool remove_from_graph, QUndoCommand* command); void SetBlockLinksSelected(Block *block, bool selected); diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 366f46277..92293cbd4 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -143,7 +143,7 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) int64_t earliest_timestamp = Timecode::time_to_timestamp(earliest_ghost, parent()->timebase()); QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, parent()->timebase(), - Timecode::CurrentDisplay()); + Core::instance()->GetTimecodeDisplay()); // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way // of the cursor) diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index e88cad283..80fd34194 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -404,7 +404,7 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po int64_t earliest_timestamp = Timecode::time_to_timestamp(time_movement, parent()->timebase()); QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, parent()->timebase(), - Timecode::CurrentDisplay(), + Core::instance()->GetTimecodeDisplay(), true); // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way diff --git a/app/widget/timelinewidget/tool/slip.cpp b/app/widget/timelinewidget/tool/slip.cpp index b13f5297e..299ed5f22 100644 --- a/app/widget/timelinewidget/tool/slip.cpp +++ b/app/widget/timelinewidget/tool/slip.cpp @@ -54,7 +54,7 @@ void TimelineWidget::SlipTool::ProcessDrag(const TimelineCoordinate &mouse_pos) int64_t earliest_timestamp = Timecode::time_to_timestamp(time_movement, parent()->timebase()); QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, parent()->timebase(), - Timecode::CurrentDisplay(), + Core::instance()->GetTimecodeDisplay(), true); // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way // of the cursor) diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index 09a9d2db8..d9b875c0e 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -623,14 +623,14 @@ void BlockSetSpeedCommand::undo_internal() block_->set_speed(old_speed_); } -TimelineRippleDeleteGapsAtRegions::TimelineRippleDeleteGapsAtRegions(ViewerOutput *vo, const TimeRangeList ®ions, QUndoCommand *parent) : +TimelineRippleDeleteGapsAtRegionsCommand::TimelineRippleDeleteGapsAtRegionsCommand(ViewerOutput *vo, const TimeRangeList ®ions, QUndoCommand *parent) : UndoCommand(parent), timeline_(vo), regions_(regions) { } -void TimelineRippleDeleteGapsAtRegions::redo_internal() +void TimelineRippleDeleteGapsAtRegionsCommand::redo_internal() { foreach (const TimeRange& range, regions_) { rational max_ripple_length = range.length(); @@ -671,7 +671,7 @@ void TimelineRippleDeleteGapsAtRegions::redo_internal() } } -void TimelineRippleDeleteGapsAtRegions::undo_internal() +void TimelineRippleDeleteGapsAtRegionsCommand::undo_internal() { for (int i=commands_.size()-1;i>=0;i--) { commands_.at(i)->undo(); @@ -679,3 +679,103 @@ void TimelineRippleDeleteGapsAtRegions::undo_internal() } commands_.empty(); } + +WorkareaSetEnabledCommand::WorkareaSetEnabledCommand(TimelinePoints *points, bool enabled, QUndoCommand *parent) : + UndoCommand(parent), + points_(points), + old_enabled_(points_->workarea()->enabled()), + new_enabled_(enabled) +{ +} + +void WorkareaSetEnabledCommand::redo_internal() +{ + points_->workarea()->set_enabled(new_enabled_); +} + +void WorkareaSetEnabledCommand::undo_internal() +{ + points_->workarea()->set_enabled(old_enabled_); +} + +WorkareaSetRangeCommand::WorkareaSetRangeCommand(TimelinePoints *points, const TimeRange &range, QUndoCommand *parent) : + UndoCommand(parent), + points_(points), + old_range_(points_->workarea()->range()), + new_range_(range) +{ +} + +void WorkareaSetRangeCommand::redo_internal() +{ + points_->workarea()->set_range(new_range_); +} + +void WorkareaSetRangeCommand::undo_internal() +{ + points_->workarea()->set_range(old_range_); +} + +BlockLinkCommand::BlockLinkCommand(Block *a, Block *b, bool link, QUndoCommand *parent) : + UndoCommand(parent), + a_(a), + b_(b), + link_(link) +{ +} + +void BlockLinkCommand::redo_internal() +{ + if (link_) { + done_ = Block::Link(a_, b_); + } else { + done_ = Block::Unlink(a_, b_); + } +} + +void BlockLinkCommand::undo_internal() +{ + if (done_) { + if (link_) { + Block::Unlink(a_, b_); + } else { + Block::Link(a_, b_); + } + } +} + +BlockUnlinkAllCommand::BlockUnlinkAllCommand(Block *block, QUndoCommand *parent) : + UndoCommand(parent), + block_(block) +{ +} + +void BlockUnlinkAllCommand::redo_internal() +{ + unlinked_ = block_->linked_clips(); + + foreach (Block* link, unlinked_) { + Block::Unlink(block_, link); + } +} + +void BlockUnlinkAllCommand::undo_internal() +{ + foreach (Block* link, unlinked_) { + Block::Link(block_, link); + } + + unlinked_.clear(); +} + +BlockLinkManyCommand::BlockLinkManyCommand(const QList blocks, bool link, QUndoCommand *parent) : + UndoCommand(parent) +{ + foreach (Block* a, blocks) { + foreach (Block* b, blocks) { + if (a != b) { + new BlockLinkCommand(a, b, link, this); + } + } + } +} diff --git a/app/widget/timelinewidget/undo/undo.h b/app/widget/timelinewidget/undo/undo.h index 8e4d81b9c..3756fe217 100644 --- a/app/widget/timelinewidget/undo/undo.h +++ b/app/widget/timelinewidget/undo/undo.h @@ -27,6 +27,7 @@ #include "node/block/gap/gap.h" #include "node/output/track/track.h" #include "node/output/track/tracklist.h" +#include "timeline/timelinepoints.h" #include "undo/undocommand.h" class BlockResizeCommand : public UndoCommand { @@ -280,9 +281,9 @@ private: }; -class TimelineRippleDeleteGapsAtRegions : public UndoCommand { +class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { public: - TimelineRippleDeleteGapsAtRegions(ViewerOutput* vo, const TimeRangeList& regions, QUndoCommand* parent = nullptr); + TimelineRippleDeleteGapsAtRegionsCommand(ViewerOutput* vo, const TimeRangeList& regions, QUndoCommand* parent = nullptr); protected: virtual void redo_internal() override; @@ -296,4 +297,77 @@ private: }; +class WorkareaSetEnabledCommand : public UndoCommand { +public: + WorkareaSetEnabledCommand(TimelinePoints* points, bool enabled, QUndoCommand* parent = nullptr); + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + TimelinePoints* points_; + + bool old_enabled_; + + bool new_enabled_; + +}; + +class WorkareaSetRangeCommand : public UndoCommand { +public: + WorkareaSetRangeCommand(TimelinePoints* points, const TimeRange& range, QUndoCommand* parent = nullptr); + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + TimelinePoints* points_; + + TimeRange old_range_; + + TimeRange new_range_; + +}; + +class BlockLinkManyCommand : public UndoCommand { +public: + BlockLinkManyCommand(const QList blocks, bool link, QUndoCommand* parent = nullptr); +}; + +class BlockLinkCommand : public UndoCommand { +public: + BlockLinkCommand(Block* a, Block* b, bool link, QUndoCommand* parent = nullptr); + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + Block* a_; + + Block* b_; + + bool link_; + + bool done_; + +}; + +class BlockUnlinkAllCommand : public UndoCommand { +public: + BlockUnlinkAllCommand(Block* block, QUndoCommand* parent = nullptr); + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + Block* block_; + + QVector unlinked_; + +}; + #endif // TIMELINEUNDOABLE_H diff --git a/app/widget/timelinewidget/view/timelineplayhead.cpp b/app/widget/timelinewidget/view/timelineplayhead.cpp index 0b9ea95a9..f15e9cf2d 100644 --- a/app/widget/timelinewidget/view/timelineplayhead.cpp +++ b/app/widget/timelinewidget/view/timelineplayhead.cpp @@ -22,12 +22,12 @@ #include -const QColor &TimelinePlayhead::PlayheadColor() const +const QColor &TimelinePlayhead::GetPlayheadColor() const { return playhead_color_; } -const QColor &TimelinePlayhead::PlayheadHighlightColor() const +const QColor &TimelinePlayhead::GetPlayheadHighlightColor() const { return playhead_highlight_color_; } @@ -45,10 +45,10 @@ void TimelinePlayhead::SetPlayheadHighlightColor(QColor c) void TimelinePlayhead::Draw(QPainter* painter, const QRectF& playhead_rect) const { painter->setPen(Qt::NoPen); - painter->setBrush(PlayheadHighlightColor()); + painter->setBrush(GetPlayheadHighlightColor()); painter->drawRect(playhead_rect); - painter->setPen(PlayheadColor()); + painter->setPen(GetPlayheadColor()); painter->setBrush(Qt::NoBrush); painter->drawLine(QLineF(playhead_rect.topLeft(), playhead_rect.bottomLeft())); } diff --git a/app/widget/timelinewidget/view/timelineplayhead.h b/app/widget/timelinewidget/view/timelineplayhead.h index bbdf1f934..15b2f0031 100644 --- a/app/widget/timelinewidget/view/timelineplayhead.h +++ b/app/widget/timelinewidget/view/timelineplayhead.h @@ -31,13 +31,13 @@ class TimelinePlayhead : public QWidget { Q_OBJECT - Q_PROPERTY(QColor playheadColor READ PlayheadColor WRITE SetPlayheadColor DESIGNABLE true) - Q_PROPERTY(QColor playheadHighlightColor READ PlayheadHighlightColor WRITE SetPlayheadHighlightColor DESIGNABLE true) + Q_PROPERTY(QColor playheadColor READ GetPlayheadColor WRITE SetPlayheadColor DESIGNABLE true) + Q_PROPERTY(QColor playheadHighlightColor READ GetPlayheadHighlightColor WRITE SetPlayheadHighlightColor DESIGNABLE true) public: TimelinePlayhead() = default; - const QColor& PlayheadColor() const; - const QColor& PlayheadHighlightColor() const; + const QColor& GetPlayheadColor() const; + const QColor& GetPlayheadHighlightColor() const; void SetPlayheadColor(QColor c); void SetPlayheadHighlightColor(QColor c); diff --git a/app/widget/timelinewidget/view/timelineviewblockitem.cpp b/app/widget/timelinewidget/view/timelineviewblockitem.cpp index 399f82186..c70c979da 100644 --- a/app/widget/timelinewidget/view/timelineviewblockitem.cpp +++ b/app/widget/timelinewidget/view/timelineviewblockitem.cpp @@ -29,11 +29,10 @@ #include #include -#include "audio/sumsamples.h" -#include "common/clamp.h" #include "common/qtutils.h" #include "config/config.h" #include "node/block/transition/transition.h" +#include "widget/viewer/audiowaveformview.h" TimelineViewBlockItem::TimelineViewBlockItem(Block *block, QGraphicsItem* parent) : TimelineViewRect(parent), @@ -94,45 +93,12 @@ void TimelineViewBlockItem::paint(QPainter *painter, const QStyleOptionGraphicsI QByteArray w = wave_file.readAll(); // FIXME: Hardcoded channel count - int channels = 2; - - const SampleSummer::Sum* samples = reinterpret_cast(w.constData()); - int nb_samples = w.size() / sizeof(SampleSummer::Sum); - - int sample_index, next_sample_index = 0; - - QVector summary; - int summary_index = -1; - - int channel_height = rect().height() / channels; - int channel_half_height = channel_height / 2; - - for (int i=0;i(SampleSummer::kSumSampleRate) * static_cast(i+1) / this->GetScale()) * channels); - - if (summary_index != sample_index) { - summary = SampleSummer::ReSumSamples(&samples[sample_index], - qMax(channels, next_sample_index - sample_index), - channels); - summary_index = sample_index; - } - - for (int j=0;jdrawLine(i, - channel_mid + clamp(qRound(summary.at(j).min * channel_half_height), -channel_half_height, channel_half_height), - i, - channel_mid + clamp(qRound(summary.at(j).max * channel_half_height), -channel_half_height, channel_half_height)); - } - } + AudioWaveformView::DrawWaveform(painter, + rect().toRect(), + this->GetScale(), + reinterpret_cast(w.constData()), + w.size() / sizeof(SampleSummer::Sum), + 2); wave_file.close(); } diff --git a/app/widget/timeruler/CMakeLists.txt b/app/widget/timeruler/CMakeLists.txt index 9fe06158f..9c7882713 100644 --- a/app/widget/timeruler/CMakeLists.txt +++ b/app/widget/timeruler/CMakeLists.txt @@ -16,6 +16,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + widget/timeruler/seekablewidget.h + widget/timeruler/seekablewidget.cpp widget/timeruler/timeruler.h widget/timeruler/timeruler.cpp PARENT_SCOPE diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp new file mode 100644 index 000000000..624261979 --- /dev/null +++ b/app/widget/timeruler/seekablewidget.cpp @@ -0,0 +1,201 @@ +#include "seekablewidget.h" + +#include +#include +#include + +#include "common/qtutils.h" + +SeekableWidget::SeekableWidget(QWidget* parent) : + TimelineScaledWidget(parent), + time_(0), + timeline_points_(nullptr), + scroll_(0) +{ + QFontMetrics fm = fontMetrics(); + + text_height_ = fm.height(); + + // Set width of playhead marker + playhead_width_ = QFontMetricsWidth(fm, "H"); +} + +void SeekableWidget::ConnectTimelinePoints(TimelinePoints *points) +{ + if (timeline_points_) { + disconnect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, static_cast(&SeekableWidget::update)); + disconnect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&SeekableWidget::update)); + } + + timeline_points_ = points; + + if (timeline_points_) { + connect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, static_cast(&SeekableWidget::update)); + connect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&SeekableWidget::update)); + } + + update(); +} + +const int64_t &SeekableWidget::GetTime() const +{ + return time_; +} + +const int &SeekableWidget::GetScroll() const +{ + return scroll_; +} + +void SeekableWidget::mousePressEvent(QMouseEvent *event) +{ + SeekToScreenPoint(event->pos().x()); +} + +void SeekableWidget::mouseMoveEvent(QMouseEvent *event) +{ + if (event->buttons() & Qt::LeftButton) { + SeekToScreenPoint(event->pos().x()); + } +} + +void SeekableWidget::ScaleChangedEvent(const double &) +{ + update(); +} + +TimelinePoints *SeekableWidget::timeline_points() const +{ + return timeline_points_; +} + +void SeekableWidget::SetTime(const int64_t &r) +{ + time_ = r; + + update(); +} + +void SeekableWidget::SetScroll(int s) +{ + scroll_ = s; + + update(); +} + +double SeekableWidget::ScreenToUnitFloat(int screen) +{ + return (screen + scroll_) / GetScale() / timebase_dbl(); +} + +int64_t SeekableWidget::ScreenToUnit(int screen) +{ + return qFloor(ScreenToUnitFloat(screen)); +} + +int64_t SeekableWidget::ScreenToUnitRounded(int screen) +{ + return qRound64(ScreenToUnitFloat(screen)); +} + +int SeekableWidget::UnitToScreen(int64_t unit) +{ + return qFloor(static_cast(unit) * GetScale() * timebase_dbl()) - scroll_; +} + +int SeekableWidget::TimeToScreen(const rational &time) +{ + return qFloor(time.toDouble() * GetScale()) - scroll_; +} + +void SeekableWidget::SeekToScreenPoint(int screen) +{ + int64_t timestamp = qMax(static_cast(0), ScreenToUnitRounded(screen)); + + SetTime(timestamp); + + emit TimeChanged(timestamp); +} + +void SeekableWidget::DrawTimelinePoints(QPainter* p, int marker_bottom) +{ + if (!timeline_points()) { + return; + } + + // Draw in/out workarea + if (timeline_points()->workarea()->enabled()) { + int workarea_left = qMax(0, TimeToScreen(timeline_points()->workarea()->in())); + int workarea_right; + + if (timeline_points()->workarea()->out() == TimelineWorkArea::kResetOut) { + workarea_right = width(); + } else { + workarea_right = qMin(width(), TimeToScreen(timeline_points()->workarea()->out())); + } + + p->fillRect(workarea_left, 0, workarea_right - workarea_left, height(), palette().highlight()); + } + + // Draw markers + if (marker_bottom > 0 && !timeline_points()->markers()->list().isEmpty()) { + + int marker_top = marker_bottom - text_height_; + + // FIXME: Hardcoded marker colors + p->setPen(Qt::black); + p->setBrush(Qt::green); + + foreach (TimelineMarker* marker, timeline_points()->markers()->list()) { + int marker_left = TimeToScreen(marker->time().in()); + int marker_right = TimeToScreen(marker->time().out()); + + if (marker_left >= width() || marker_right < 0) { + continue; + } + + if (marker->time().length() == 0) { + // Single point in time marker + DrawPlayhead(p, marker_left, marker_bottom); + } else { + // Marker range + int rect_left = qMax(0, marker_left); + int rect_right = qMin(width(), marker_right); + + QRect marker_rect(rect_left, marker_top, rect_right - rect_left, marker_bottom - marker_top); + + p->drawRect(marker_rect); + + if (!marker->name().isEmpty()) { + p->drawText(marker_rect, marker->name()); + } + } + } + } +} + +void SeekableWidget::DrawPlayhead(QPainter *p, int x, int y) +{ + int half_width = playhead_width_ / 2; + + if (x + half_width < 0 || x - half_width > width()) { + return; + } + + p->setRenderHint(QPainter::Antialiasing); + + int half_text_height = text_height() / 3; + + QPoint points[] = { + QPoint(x, y), + QPoint(x - half_width, y - half_text_height), + QPoint(x - half_width, y - text_height()), + QPoint(x + 1 + half_width, y - text_height()), + QPoint(x + 1 + half_width, y - half_text_height), + QPoint(x + 1, y), + }; + + p->drawPolygon(points, 6); + + p->setRenderHint(QPainter::Antialiasing, false); +} diff --git a/app/widget/timeruler/seekablewidget.h b/app/widget/timeruler/seekablewidget.h new file mode 100644 index 000000000..e96c23b53 --- /dev/null +++ b/app/widget/timeruler/seekablewidget.h @@ -0,0 +1,88 @@ +#ifndef SEEKABLEWIDGET_H +#define SEEKABLEWIDGET_H + +#include "common/rational.h" +#include "timeline/timelinepoints.h" +#include "widget/timelinewidget/view/timelineplayhead.h" +#include "widget/timelinewidget/timelinescaledobject.h" + +class SeekableWidget : public TimelineScaledWidget +{ + Q_OBJECT +public: + SeekableWidget(QWidget *parent = nullptr); + + const int64_t& GetTime() const; + + const int& GetScroll() const; + + void ConnectTimelinePoints(TimelinePoints* points); + +public slots: + void SetTime(const int64_t &r); + + void SetScroll(int s); + +protected: + void SeekToScreenPoint(int screen); + + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + + virtual void ScaleChangedEvent(const double&) override; + + void DrawTimelinePoints(QPainter *p, int marker_bottom = 0); + + TimelinePoints* timeline_points() const; + + double ScreenToUnitFloat(int screen); + + int64_t ScreenToUnit(int screen); + int64_t ScreenToUnitRounded(int screen); + + int UnitToScreen(int64_t unit); + + int TimeToScreen(const rational& time); + + void DrawPlayhead(QPainter* p, int x, int y); + + inline const int& text_height() const { + return text_height_; + } + + inline const int& playhead_width() const { + return playhead_width_; + } + + inline const QColor& GetPlayheadColor() const + { + return style_.GetPlayheadColor(); + } + + inline const QColor& GetPlayheadHighlightColor() const + { + return style_.GetPlayheadHighlightColor(); + } + +signals: + /** + * @brief Signal emitted whenever the time changes on this ruler, either by user or programatically + */ + void TimeChanged(int64_t); + +private: + int64_t time_; + + TimelinePlayhead style_; + + TimelinePoints* timeline_points_; + + int scroll_; + + int text_height_; + + int playhead_width_; + +}; + +#endif // SEEKABLEWIDGET_H diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index a5ef88a9a..25b504995 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -21,9 +21,7 @@ #include "timeruler.h" #include -#include #include -#include #include "common/timecodefunctions.h" #include "common/qtutils.h" @@ -31,12 +29,9 @@ #include "core.h" TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* parent) : - QWidget(parent), - scroll_(0), + SeekableWidget(parent), text_visible_(text_visible), centered_text_(true), - scale_(1.0), - time_(0), show_cache_status_(cache_status_visible) { QFontMetrics fm = fontMetrics(); @@ -44,46 +39,17 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* pare setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); // Text height is used to calculate widget height - text_height_ = fm.height(); - cache_status_height_ = text_height_ / 4; + cache_status_height_ = text_height() / 4; // Get the "minimum" space allowed between two line markers on the ruler (in screen pixels) // Mediocre but reliable way of scaling UI objects by font/DPI size minimum_gap_between_lines_ = QFontMetricsWidth(fm, "H"); - // Set width of playhead marker - playhead_width_ = minimum_gap_between_lines_; - // Text visibility affects height, so we set that here UpdateHeight(); -} -const double &TimeRuler::GetScale() -{ - return scale_; -} - -void TimeRuler::SetScale(const double &d) -{ - scale_ = d; - - update(); -} - -void TimeRuler::SetTimebase(const rational &r) -{ - timebase_ = r; - - timebase_dbl_ = timebase_.toDouble(); - - timebase_flipped_dbl_ = timebase_.flipped().toDouble(); - - update(); -} - -const int64_t &TimeRuler::GetTime() -{ - return time_; + // Force update if the default timecode display mode changes + connect(Core::instance(), &Core::TimecodeDisplayChanged, this, static_cast(&TimeRuler::update)); } void TimeRuler::SetCacheStatusLength(const rational &length) @@ -97,20 +63,6 @@ void TimeRuler::SetCacheStatusLength(const rational &length) } } -void TimeRuler::SetTime(const int64_t &r) -{ - time_ = r; - - update(); -} - -void TimeRuler::SetScroll(int s) -{ - scroll_ = s; - - update(); -} - void TimeRuler::CacheInvalidatedRange(const TimeRange& range) { if (show_cache_status_) { @@ -123,7 +75,7 @@ void TimeRuler::CacheInvalidatedRange(const TimeRange& range) void TimeRuler::CacheTimeReady(const rational &time) { if (show_cache_status_) { - dirty_cache_ranges_.RemoveTimeRange(TimeRange(time, time + timebase_)); + dirty_cache_ranges_.RemoveTimeRange(TimeRange(time, time + timebase())); update(); } @@ -132,18 +84,33 @@ void TimeRuler::CacheTimeReady(const rational &time) void TimeRuler::paintEvent(QPaintEvent *) { // Nothing to paint if the timebase is invalid - if (timebase_.isNull()) { + if (timebase().isNull()) { return; } QPainter p(this); - double width_of_frame = timebase_dbl_ * scale_; + // Draw timeline points if connected + if (timeline_points()) { + int marker_bottom = height() - text_height(); + + if (show_cache_status_) { + marker_bottom -= cache_status_height_; + } + + if (text_visible_) { + marker_bottom -= cache_status_height_; + } + + DrawTimelinePoints(&p, marker_bottom); + } + + double width_of_frame = timebase_dbl() * GetScale(); double width_of_second = 0; do { - width_of_second += timebase_dbl_; + width_of_second += timebase_dbl(); } while (width_of_second < 1.0); - width_of_second *= scale_; + width_of_second *= GetScale(); double width_of_minute = width_of_second * 60; double width_of_hour = width_of_minute * 60; double width_of_day = width_of_hour * 24; @@ -229,7 +196,7 @@ void TimeRuler::paintEvent(QPaintEvent *) const int kAverageTextWidth = 200; for (int i=-kAverageTextWidth;i(i + scroll_); + double screen_pt = static_cast(i + GetScroll()); if (long_interval > -1) { int this_long_unit = qFloor(screen_pt/long_interval); @@ -239,7 +206,7 @@ void TimeRuler::paintEvent(QPaintEvent *) if (text_visible_) { QRect text_rect; Qt::Alignment text_align; - QString timecode_str = Timecode::timestamp_to_timecode(ScreenToUnit(i), timebase_, Timecode::CurrentDisplay()); + QString timecode_str = Timecode::timestamp_to_timecode(ScreenToUnit(i), timebase(), Core::instance()->GetTimecodeDisplay()); int timecode_width = QFontMetricsWidth(fm, timecode_str); int timecode_left; @@ -308,43 +275,17 @@ void TimeRuler::paintEvent(QPaintEvent *) } // Draw the playhead if it's on screen at the moment - int playhead_pos = UnitToScreen(time_); - if (playhead_pos + playhead_width_ >= 0 && playhead_pos - playhead_width_ < width()) { - p.setPen(Qt::NoPen); - p.setBrush(style_.PlayheadColor()); - DrawPlayhead(&p, playhead_pos, line_bottom); - } + int playhead_pos = UnitToScreen(GetTime()); + p.setPen(Qt::NoPen); + p.setBrush(GetPlayheadColor()); + DrawPlayhead(&p, playhead_pos, line_bottom); } -void TimeRuler::mousePressEvent(QMouseEvent *event) +void TimeRuler::TimebaseChangedEvent(const rational &tb) { - SeekToScreenPoint(event->pos().x()); -} + timebase_flipped_dbl_ = tb.flipped().toDouble(); -void TimeRuler::mouseMoveEvent(QMouseEvent *event) -{ - if (event->buttons() & Qt::LeftButton) { - SeekToScreenPoint(event->pos().x()); - } -} - -void TimeRuler::DrawPlayhead(QPainter *p, int x, int y) -{ - p->setRenderHint(QPainter::Antialiasing); - - int half_text_height = text_height_ / 3; - int half_width = playhead_width_ / 2; - - QPoint points[] = { - QPoint(x, y), - QPoint(x - half_width, y - half_text_height), - QPoint(x - half_width, y - text_height_), - QPoint(x + 1 + half_width, y - text_height_), - QPoint(x + 1 + half_width, y - half_text_height), - QPoint(x + 1, y), - }; - - p->drawPolygon(points, 6); + update(); } int TimeRuler::CacheStatusHeight() const @@ -352,46 +293,22 @@ int TimeRuler::CacheStatusHeight() const return fontMetrics().height() / 4; } -double TimeRuler::ScreenToUnitFloat(int screen) -{ - return (screen + scroll_) / scale_ / timebase_dbl_; -} - -int64_t TimeRuler::ScreenToUnit(int screen) -{ - return qFloor(ScreenToUnitFloat(screen)); -} - -int TimeRuler::UnitToScreen(int64_t unit) -{ - return qFloor(static_cast(unit) * scale_ * timebase_dbl_) - scroll_; -} - -int TimeRuler::TimeToScreen(const rational &time) -{ - return qFloor(time.toDouble() * scale_) - scroll_; -} - -void TimeRuler::SeekToScreenPoint(int screen) -{ - int64_t timestamp = qMax(0, qRound(ScreenToUnitFloat(screen))); - - SetTime(timestamp); - - emit TimeChanged(timestamp); -} - void TimeRuler::UpdateHeight() { - int height = text_height_; + int height = text_height(); + // Add text height if (text_visible_) { - height += text_height_; + height += text_height(); } + // Add cache status height if (show_cache_status_) { height += cache_status_height_; } + // Add marker height + height += text_height(); + setFixedHeight(height); } diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index 706d98e29..83ada935e 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -24,30 +24,18 @@ #include #include -#include "common/rational.h" #include "common/timerange.h" -#include "widget/timelinewidget/view/timelineplayhead.h" +#include "seekablewidget.h" -class TimeRuler : public QWidget +class TimeRuler : public SeekableWidget { Q_OBJECT public: TimeRuler(bool text_visible = true, bool cache_status_visible = false, QWidget* parent = nullptr); - const double& GetScale(); - void SetScale(const double& d); - - void SetTimebase(const rational& r); - void SetCenteredText(bool c); - const int64_t& GetTime(); - public slots: - void SetTime(const int64_t &r); - - void SetScroll(int s); - void CacheInvalidatedRange(const TimeRange &range); void CacheTimeReady(const rational& time); @@ -57,58 +45,23 @@ public slots: protected: virtual void paintEvent(QPaintEvent* e) override; - virtual void mousePressEvent(QMouseEvent *event) override; - virtual void mouseMoveEvent(QMouseEvent *event) override; - -signals: - /** - * @brief Signal emitted whenever the time changes on this ruler, either by user or programmatically - */ - void TimeChanged(int64_t); + virtual void TimebaseChangedEvent(const rational& tb) override; private: void UpdateHeight(); - void DrawPlayhead(QPainter* p, int x, int y); - int CacheStatusHeight() const; - double ScreenToUnitFloat(int screen); - - int64_t ScreenToUnit(int screen); - - int UnitToScreen(int64_t unit); - - int TimeToScreen(const rational& time); - - void SeekToScreenPoint(int screen); - - int text_height_; - int cache_status_height_; int minimum_gap_between_lines_; - int playhead_width_; - - int scroll_; - bool text_visible_; bool centered_text_; - double scale_; - - rational timebase_; - - double timebase_dbl_; - double timebase_flipped_dbl_; - int64_t time_; - - TimelinePlayhead style_; - bool show_cache_status_; rational cache_length_; diff --git a/app/widget/viewer/CMakeLists.txt b/app/widget/viewer/CMakeLists.txt index c5cf17d3b..16693f4de 100644 --- a/app/widget/viewer/CMakeLists.txt +++ b/app/widget/viewer/CMakeLists.txt @@ -16,6 +16,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + widget/viewer/audiowaveformview.h + widget/viewer/audiowaveformview.cpp widget/viewer/footageviewer.h widget/viewer/footageviewer.cpp widget/viewer/viewer.h diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp new file mode 100644 index 000000000..dd7ac643b --- /dev/null +++ b/app/widget/viewer/audiowaveformview.cpp @@ -0,0 +1,189 @@ +#include "audiowaveformview.h" + +#include +#include +#include + +#include "common/clamp.h" +#include "config/config.h" + +AudioWaveformView::AudioWaveformView(QWidget *parent) : + SeekableWidget(parent), + backend_(nullptr) +{ + setAutoFillBackground(true); + setBackgroundRole(QPalette::Base); +} + +void AudioWaveformView::SetBackend(AudioRenderBackend *backend) +{ + if (backend_) { + disconnect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&AudioWaveformView::update)); + disconnect(backend_, &AudioRenderBackend::ParamsChanged, this, &AudioWaveformView::BackendParamsChanged); + + SetTimebase(0); + } + + backend_ = backend; + + if (backend_) { + connect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&AudioWaveformView::update)); + connect(backend_, &AudioRenderBackend::ParamsChanged, this, &AudioWaveformView::BackendParamsChanged); + + SetTimebase(backend_->params().time_base()); + } + + update(); +} + +void AudioWaveformView::DrawWaveform(QPainter *painter, const QRect& rect, const double& scale, const SampleSummer::Sum* samples, int nb_samples, int channels) +{ + int sample_index, next_sample_index = 0; + + QVector summary; + int summary_index = -1; + + int channel_height = rect.height() / channels; + int channel_half_height = channel_height / 2; + + for (int i=0;i(SampleSummer::kSumSampleRate) * static_cast(i+1) / scale) * channels); + + if (summary_index != sample_index) { + summary = SampleSummer::ReSumSamples(&samples[sample_index], + qMax(channels, next_sample_index - sample_index), + channels); + summary_index = sample_index; + } + + int line_x = i + rect.x(); + + for (int j=0;jdrawLine(line_x, + channel_bottom - diff, + line_x, + channel_bottom); + } else{ + int channel_mid = rect.y() + channel_height * j + channel_half_height; + + painter->drawLine(line_x, + channel_mid + clamp(qRound(summary.at(j).min * channel_half_height), -channel_half_height, channel_half_height), + line_x, + channel_mid + clamp(qRound(summary.at(j).max * channel_half_height), -channel_half_height, channel_half_height)); + } + } + } +} + +void AudioWaveformView::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + if (!backend_ || backend_->CachePathName().isEmpty() || !backend_->params().is_valid()) { + return; + } + + const AudioRenderingParams& params = backend_->params(); + + if (cached_size_ != size() + || cached_scale_ != GetScale() + || cached_scroll_ != GetScroll()) { + + cached_waveform_ = QPixmap(size()); + cached_waveform_.fill(Qt::transparent); + + QFile fs(backend_->CachePathName()); + + if (fs.open(QFile::ReadOnly)) { + + QPainter wave_painter(&cached_waveform_); + + // FIXME: Hardcoded color + wave_painter.setPen(Qt::green); + + int channel_height = height() / params.channel_count(); + int channel_half_height = channel_height / 2; + + int drew = 0; + + fs.seek(params.samples_to_bytes(ScreenToUnitRounded(0))); + + for (int x=0; x samples = SampleSummer::SumSamples(reinterpret_cast(read_buffer.constData()), + samples_len, + params.channel_count()); + + for (int i=0;i(channel_half_height), + x, + channel_mid + samples.at(i).max * static_cast(channel_half_height)); + } + + drew++; + } + } + + cached_size_ = size(); + cached_scale_ = GetScale(); + cached_scroll_ = GetScroll(); + + fs.close(); + + } + } + + QPainter p(this); + + // Draw in/out points + DrawTimelinePoints(&p); + + // Draw cached waveform pixmap + p.drawPixmap(0, 0, cached_waveform_); + + // Draw playhead + p.setPen(GetPlayheadColor()); + + int playhead_x = UnitToScreen(GetTime()); + p.drawLine(playhead_x, 0, playhead_x, height()); +} + +void AudioWaveformView::BackendParamsChanged() +{ + SetTimebase(backend_->params().time_base()); +} diff --git a/app/widget/viewer/audiowaveformview.h b/app/widget/viewer/audiowaveformview.h new file mode 100644 index 000000000..283670c1d --- /dev/null +++ b/app/widget/viewer/audiowaveformview.h @@ -0,0 +1,39 @@ +#ifndef WAVEFORMVIEW_H +#define WAVEFORMVIEW_H + +#include + +#include "audio/sumsamples.h" +#include "render/audioparams.h" +#include "render/backend/audiorenderbackend.h" +#include "widget/timeruler/seekablewidget.h" + +class AudioWaveformView : public SeekableWidget +{ + Q_OBJECT +public: + AudioWaveformView(QWidget* parent = nullptr); + + //void SetData(const QString& file, const AudioRenderingParams& params); + + void SetBackend(AudioRenderBackend* backend); + + static void DrawWaveform(QPainter* painter, const QRect &rect, const double &scale, const SampleSummer::Sum *samples, int nb_samples, int channels); + +protected: + virtual void paintEvent(QPaintEvent* event) override; + +private: + AudioRenderBackend* backend_; + + QPixmap cached_waveform_; + QSize cached_size_; + double cached_scale_; + int cached_scroll_; + +private slots: + void BackendParamsChanged(); + +}; + +#endif // WAVEFORMVIEW_H diff --git a/app/widget/viewer/footageviewer.cpp b/app/widget/viewer/footageviewer.cpp index 5a4c4f860..926806f3d 100644 --- a/app/widget/viewer/footageviewer.cpp +++ b/app/widget/viewer/footageviewer.cpp @@ -13,10 +13,6 @@ FootageViewerWidget::FootageViewerWidget(QWidget *parent) : audio_node_ = new AudioInput(); viewer_node_ = new ViewerOutput(); - NodeParam::ConnectEdge(video_node_->output(), viewer_node_->texture_input()); - NodeParam::ConnectEdge(audio_node_->output(), viewer_node_->samples_input()); - NodeParam::ConnectEdge(video_node_->output(), viewer_node_->length_input()); - connect(gl_widget_, &ViewerGLWidget::DragStarted, this, &FootageViewerWidget::StartFootageDrag); } @@ -29,6 +25,9 @@ void FootageViewerWidget::SetFootage(Footage *footage) { if (footage_) { ConnectViewerNode(nullptr); + + NodeParam::DisconnectEdge(video_node_->output(), viewer_node_->texture_input()); + NodeParam::DisconnectEdge(audio_node_->output(), viewer_node_->samples_input()); } footage_ = footage; @@ -55,11 +54,13 @@ void FootageViewerWidget::SetFootage(Footage *footage) if (video_stream) { video_node_->SetFootage(video_stream); viewer_node_->set_video_params(VideoParams(video_stream->width(), video_stream->height(), video_stream->frame_rate().flipped())); + NodeParam::ConnectEdge(video_node_->output(), viewer_node_->texture_input()); } if (audio_stream) { audio_node_->SetFootage(audio_stream); viewer_node_->set_audio_params(AudioParams(audio_stream->sample_rate(), audio_stream->channel_layout())); + NodeParam::ConnectEdge(audio_node_->output(), viewer_node_->samples_input()); } ConnectViewerNode(viewer_node_, footage->project()->color_manager()); @@ -68,13 +69,17 @@ void FootageViewerWidget::SetFootage(Footage *footage) } } +TimelinePoints *FootageViewerWidget::ConnectTimelinePoints() +{ + return footage_ ? footage_ : nullptr; +} + void FootageViewerWidget::StartFootageDrag() { if (!GetFootage()) { return; } - qDebug() << "Drag start!"; QDrag* drag = new QDrag(this); QMimeData* mimedata = new QMimeData(); diff --git a/app/widget/viewer/footageviewer.h b/app/widget/viewer/footageviewer.h index 06bbd46e5..dc1634291 100644 --- a/app/widget/viewer/footageviewer.h +++ b/app/widget/viewer/footageviewer.h @@ -15,6 +15,9 @@ public: Footage* GetFootage() const; void SetFootage(Footage* footage); +protected: + virtual TimelinePoints* ConnectTimelinePoints() override; + private: Footage* footage_; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index f14080eed..d2408630b 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -47,20 +47,30 @@ ViewerWidget::ViewerWidget(QWidget *parent) : QVBoxLayout* layout = new QVBoxLayout(this); layout->setMargin(0); - // Create main OpenGL-based view + // Set up stacked widget to allow switching away from the viewer widget + stack_ = new QStackedWidget(); + layout->addWidget(stack_); + + // Create main OpenGL-based view and sizer sizer_ = new ViewerSizer(); - layout->addWidget(sizer_); + stack_->addWidget(sizer_); gl_widget_ = new ViewerGLWidget(); connect(gl_widget_, &ViewerGLWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); + connect(sizer_, &ViewerSizer::RequestMatrix, gl_widget_, &ViewerGLWidget::SetMatrix); sizer_->SetWidget(gl_widget_); + // Create waveform view when audio is connected and video isn't + waveform_view_ = new AudioWaveformView(); + stack_->addWidget(waveform_view_); + // Create time ruler layout->addWidget(ruler()); // Create scrollbar layout->addWidget(scrollbar()); connect(scrollbar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); + connect(scrollbar(), &QScrollBar::valueChanged, waveform_view_, &AudioWaveformView::SetScroll); // Create lower controls controls_ = new PlaybackControls(); @@ -85,6 +95,9 @@ ViewerWidget::ViewerWidget(QWidget *parent) : connect(video_renderer_, &VideoRenderBackend::RangeInvalidated, ruler(), &TimeRuler::CacheInvalidatedRange); audio_renderer_ = new AudioBackend(this); + waveform_view_->SetBackend(audio_renderer_); + connect(waveform_view_, &AudioWaveformView::TimeChanged, this, &ViewerWidget::SetTimeAndSignal); + connect(PixelFormat::instance(), &PixelFormat::FormatChanged, this, &ViewerWidget::UpdateRendererParameters); SetAutoMaxScrollBar(true); @@ -97,6 +110,7 @@ void ViewerWidget::TimeChangedEvent(const int64_t &i) } controls_->SetTime(i); + waveform_view_->SetTime(i); if (GetConnectedNode() && last_time_ != i) { rational time_set = Timecode::timestamp_to_time(i, timebase()); @@ -111,13 +125,21 @@ void ViewerWidget::TimeChangedEvent(const int64_t &i) void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) { - SetTimebase(n->video_params().time_base()); + if (!n->video_params().time_base().isNull()) { + SetTimebase(n->video_params().time_base()); + } else if (n->audio_params().sample_rate() > 0) { + SetTimebase(n->audio_params().time_base()); + } else { + SetTimebase(rational()); + } connect(n, &ViewerOutput::TimebaseChanged, this, &ViewerWidget::SetTimebase); connect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SizeChangedSlot); connect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot); connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererParameters); connect(n, &ViewerOutput::VisibleInvalidated, this, &ViewerWidget::InvalidateVisible); + connect(n, &ViewerOutput::VideoGraphChanged, this, &ViewerWidget::UpdateStack); + connect(n, &ViewerOutput::AudioGraphChanged, this, &ViewerWidget::UpdateStack); SizeChangedSlot(n->video_params().width(), n->video_params().height()); LengthChangedSlot(n->Length()); @@ -133,24 +155,34 @@ void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) divider_ = CalculateDivider(); UpdateRendererParameters(); + + UpdateStack(); + + if (GetConnectedTimelinePoints()) { + waveform_view_->ConnectTimelinePoints(GetConnectedTimelinePoints()); + } } void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n) { Pause(); - SetTimebase(0); + SetTimebase(rational()); disconnect(n, &ViewerOutput::TimebaseChanged, this, &ViewerWidget::SetTimebase); disconnect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SizeChangedSlot); disconnect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot); disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererParameters); disconnect(n, &ViewerOutput::VisibleInvalidated, this, &ViewerWidget::InvalidateVisible); + disconnect(n, &ViewerOutput::VideoGraphChanged, this, &ViewerWidget::UpdateStack); + disconnect(n, &ViewerOutput::AudioGraphChanged, this, &ViewerWidget::UpdateStack); // Effectively disables the viewer and clears the state SizeChangedSlot(0, 0); gl_widget_->DisconnectColorManager(); + + waveform_view_->ConnectTimelinePoints(nullptr); } void ViewerWidget::ConnectedNodeChanged(ViewerOutput *n) @@ -159,6 +191,13 @@ void ViewerWidget::ConnectedNodeChanged(ViewerOutput *n) audio_renderer_->SetViewerNode(n); } +void ViewerWidget::ScaleChangedEvent(const double &s) +{ + TimeBasedWidget::ScaleChangedEvent(s); + + waveform_view_->SetScale(s); +} + void ViewerWidget::resizeEvent(QResizeEvent *event) { TimeBasedWidget::resizeEvent(event); @@ -169,6 +208,8 @@ void ViewerWidget::resizeEvent(QResizeEvent *event) UpdateRendererParameters(); } + + UpdateMinimumScale(); } void ViewerWidget::TogglePlayPause() @@ -251,7 +292,11 @@ void ViewerWidget::PlayInternal(int speed) controls_->ShowPauseButton(); - connect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); + if (stack_->currentWidget() == sizer_) { + connect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); + } else { + connect(AudioManager::instance(), &AudioManager::OutputNotified, this, &ViewerWidget::PlaybackTimerUpdate); + } } void ViewerWidget::PushScrubbedAudio() @@ -261,8 +306,8 @@ void ViewerWidget::PushScrubbedAudio() QIODevice* audio_src = audio_renderer_->GetAudioPullDevice(); if (audio_src && audio_src->open(QFile::ReadOnly)) { - // Try to get one "frame" of audio - int size_of_sample = audio_renderer_->params().time_to_bytes(timebase()); + // FIXME: Hardcoded scrubbing interval (20ms) + int size_of_sample = audio_renderer_->params().time_to_bytes(rational(20, 1000)); // Push audio audio_src->seek(audio_renderer_->params().time_to_bytes(GetTime())); @@ -287,6 +332,24 @@ int ViewerWidget::CalculateDivider() return divider_; } +void ViewerWidget::UpdateMinimumScale() +{ + if (!GetConnectedNode()) { + return; + } + + SetMinimumScale(static_cast(ruler()->width()) / GetConnectedNode()->Length().toDouble()); +} + +void ViewerWidget::UpdateStack() +{ + if (!GetConnectedNode() || GetConnectedNode()->texture_input()->IsConnected()) { + stack_->setCurrentWidget(sizer_); + } else { + stack_->setCurrentWidget(waveform_view_); + } +} + void ViewerWidget::UpdateRendererParameters() { if (!GetConnectedNode()) { @@ -359,12 +422,21 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) // Playback resolution QMenu* playback_resolution_menu = menu.addMenu(tr("Resolution")); playback_resolution_menu->addAction(tr("Full"))->setData(1); - playback_resolution_menu->addAction(tr("1/2"))->setData(2); - playback_resolution_menu->addAction(tr("1/4"))->setData(4); - playback_resolution_menu->addAction(tr("1/8"))->setData(8); - playback_resolution_menu->addAction(tr("1/16"))->setData(16); + int dividers[] = {2, 4, 8, 16}; + for (int i=0;i<4;i++) { + playback_resolution_menu->addAction(tr("1/%1").arg(dividers[i]))->setData(dividers[i]); + } connect(playback_resolution_menu, &QMenu::triggered, this, &ViewerWidget::SetDividerFromMenu); + // Viewer Zoom Level + QMenu* zoom_menu = menu.addMenu(tr("Zoom")); + int zoom_levels[] = {10, 25, 50, 75, 100, 150, 200, 400}; + zoom_menu->addAction(tr("Fit"))->setData(0); + for (int i=0;i<8;i++) { + zoom_menu->addAction(tr("%1%").arg(zoom_levels[i]))->setData(zoom_levels[i]); + } + connect(zoom_menu, &QMenu::triggered, this, &ViewerWidget::SetZoomFromMenu); + foreach (QAction* a, playback_resolution_menu->actions()) { a->setCheckable(true); if (a->data() == divider_) { @@ -387,7 +459,11 @@ void ViewerWidget::Pause() playback_speed_ = 0; controls_->ShowPlayButton(); - disconnect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); + if (stack_->currentWidget() == sizer_) { + disconnect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); + } else { + disconnect(AudioManager::instance(), &AudioManager::OutputNotified, this, &ViewerWidget::PlaybackTimerUpdate); + } } } @@ -495,6 +571,7 @@ void ViewerWidget::LengthChangedSlot(const rational &length) { controls_->SetEndTime(Timecode::time_to_timestamp(length, timebase())); ruler()->SetCacheStatusLength(length); + UpdateMinimumScale(); } void ViewerWidget::ColorDisplayChanged(QAction* action) @@ -526,6 +603,11 @@ void ViewerWidget::SetDividerFromMenu(QAction *action) UpdateRendererParameters(); } +void ViewerWidget::SetZoomFromMenu(QAction *action) +{ + sizer_->SetZoom(action->data().toInt()); +} + void ViewerWidget::InvalidateVisible() { video_renderer_->InvalidateCache(TimeRange(GetTime(), GetTime())); diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index a84aebc59..4c3c642dd 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -28,6 +28,7 @@ #include #include +#include "audiowaveformview.h" #include "common/rational.h" #include "node/output/viewer/viewer.h" #include "render/backend/opengl/openglbackend.h" @@ -113,6 +114,8 @@ protected: virtual void DisconnectNodeInternal(ViewerOutput *) override; virtual void ConnectedNodeChanged(ViewerOutput*n) override; + virtual void ScaleChangedEvent(const double& s) override; + virtual void resizeEvent(QResizeEvent *event) override; OpenGLBackend* video_renderer_; @@ -131,6 +134,10 @@ private: int CalculateDivider(); + void UpdateMinimumScale(); + + QStackedWidget* stack_; + ViewerSizer* sizer_; PlaybackControls* controls_; @@ -152,6 +159,8 @@ private: bool time_changed_from_timer_; + AudioWaveformView* waveform_view_; + private slots: void PlaybackTimerUpdate(); @@ -182,8 +191,12 @@ private slots: void SetDividerFromMenu(QAction* action); + void SetZoomFromMenu(QAction* action); + void InvalidateVisible(); + void UpdateStack(); + }; #endif // VIEWER_WIDGET_H diff --git a/app/widget/viewer/viewerglwidget.cpp b/app/widget/viewer/viewerglwidget.cpp index ca34dafd8..ccd5ed595 100644 --- a/app/widget/viewer/viewerglwidget.cpp +++ b/app/widget/viewer/viewerglwidget.cpp @@ -129,6 +129,14 @@ void ViewerGLWidget::SetImage(const QString &fn) void ViewerGLWidget::SetOCIODisplay(const QString &display) { ocio_display_ = display; + + // Determine if the selected view is available in this display + if (color_manager_ + && !color_manager_->ListAvailableViews(ocio_display_).contains(ocio_view_)) { + // If not, set to the default view for this display + ocio_view_ = color_manager_->GetDefaultView(ocio_display_); + } + SetupColorProcessor(); update(); } diff --git a/app/widget/viewer/viewerglwidget.h b/app/widget/viewer/viewerglwidget.h index f2e2bd1ca..4c14fb701 100644 --- a/app/widget/viewer/viewerglwidget.h +++ b/app/widget/viewer/viewerglwidget.h @@ -68,18 +68,17 @@ public: */ void DisconnectColorManager(); - /** - * @brief Set the transformation matrix to draw with - * - * Set this if you want the drawing to pass through some sort of transform (most of the time you won't want this). - */ - void SetMatrix(const QMatrix4x4& mat); - /** * @brief Set an image to load and display on screen */ void SetImage(const QString& fn); + ColorManager* color_manager() const; + + const QString& ocio_display() const; + const QString& ocio_view() const; + const QString& ocio_look() const; + public slots: /** * @brief Set the texture to draw and draw it @@ -113,11 +112,12 @@ public slots: */ void SetOCIOLook(const QString& look); - ColorManager* color_manager() const; - - const QString& ocio_display() const; - const QString& ocio_view() const; - const QString& ocio_look() const; + /** + * @brief Set the transformation matrix to draw with + * + * Set this if you want the drawing to pass through some sort of transform (most of the time you won't want this). + */ + void SetMatrix(const QMatrix4x4& mat); signals: void DragStarted(); diff --git a/app/widget/viewer/viewersizer.cpp b/app/widget/viewer/viewersizer.cpp index a9c3c2fc1..d9d906bc1 100644 --- a/app/widget/viewer/viewersizer.cpp +++ b/app/widget/viewer/viewersizer.cpp @@ -1,9 +1,12 @@ #include "viewersizer.h" +#include + ViewerSizer::ViewerSizer(QWidget *parent) : QWidget(parent), widget_(nullptr), - aspect_ratio_(0) + aspect_ratio_(0), + zoom_(0) { } @@ -23,15 +26,25 @@ void ViewerSizer::SetWidget(QWidget *widget) void ViewerSizer::SetChildSize(int width, int height) { - if (height == 0) { + width_ = width; + height_ = height; + + if (!width_ || !height_) { aspect_ratio_ = 0; } else { - aspect_ratio_ = static_cast(width) / static_cast(height); + aspect_ratio_ = static_cast(width_) / static_cast(height_); } UpdateSize(); } +void ViewerSizer::SetZoom(int percent) +{ + zoom_ = percent; + + UpdateSize(); +} + void ViewerSizer::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); @@ -53,21 +66,51 @@ void ViewerSizer::UpdateSize() widget_->setVisible(true); - double our_aspect_ratio = static_cast(width()) / static_cast(height()); + QSize child_size; + QMatrix4x4 child_matrix; - QPoint child_pos; - QSize child_size = size(); + if (zoom_ <= 0) { + + // If zoom is 0, we auto-fit + double our_aspect_ratio = static_cast(width()) / static_cast(height()); + + child_size = size(); + + if (our_aspect_ratio > aspect_ratio_) { + // This container is wider than the image, scale by height + child_size = QSize(qRound(child_size.height() * aspect_ratio_), height()); + } else { + // This container is taller than the image, scale by width + child_size = QSize(width(), qRound(child_size.width() / aspect_ratio_)); + } - if (our_aspect_ratio > aspect_ratio_) { - // This container is wider than the image, scale by height - child_size.setWidth(qRound(child_size.height() * aspect_ratio_)); - child_pos.setX(width() / 2 - child_size.width() / 2); } else { - // This container is taller than the image, scale by width - child_size.setHeight(qRound(child_size.width() / aspect_ratio_)); - child_pos.setY(height() / 2 - child_size.height() / 2); + + float x_scale = 1.0f; + float y_scale = 1.0f; + + int zoomed_width = qRound(width_ * static_cast(zoom_) * 0.01); + int zoomed_height = qRound(height_ * static_cast(zoom_) * 0.01); + + if (zoomed_width > width()) { + x_scale = static_cast(zoomed_width) / static_cast(width()); + zoomed_width = width(); + } + + if (zoomed_height > height()) { + y_scale = static_cast(zoomed_height) / static_cast(height()); + zoomed_height = height(); + } + + // Rather than make a huge surface, we still crop at our width/height and then signal a matrix + child_matrix.scale(x_scale, y_scale, 1.0F); + + child_size = QSize(zoomed_width, zoomed_height); + } widget_->resize(child_size); - widget_->move(child_pos); + widget_->move(width() / 2 - child_size.width() / 2, height() / 2 - child_size.height() / 2); + + emit RequestMatrix(child_matrix); } diff --git a/app/widget/viewer/viewersizer.h b/app/widget/viewer/viewersizer.h index 182b61164..9b383b01f 100644 --- a/app/widget/viewer/viewersizer.h +++ b/app/widget/viewer/viewersizer.h @@ -52,6 +52,16 @@ public: */ void SetChildSize(int width, int height); + /** + * @brief Set the zoom value of the child widget + * + * The number is an integer percentage (100 = 100%). Set to 0 to auto-fit. + */ + void SetZoom(int percent); + +signals: + void RequestMatrix(const QMatrix4x4& matrix); + protected: /** * @brief Listen for resize events to ensure the child widget remains correctly sized @@ -71,11 +81,22 @@ private: */ QWidget* widget_; + /** + * @brief Internal resolution values + */ + int width_; + int height_; + /** * @brief Aspect ratio calculated from the size provided by SetChildSize() */ double aspect_ratio_; + /** + * @brief Internal zoom value + */ + int zoom_; + }; #endif // VIEWERSIZER_H diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 429a63282..1034fe378 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -87,7 +87,7 @@ MainMenu::MainMenu(QMainWindow *parent) : edit_delete_inout_item_ = edit_menu_->AddItem("deleteinout", nullptr, nullptr, ";"); edit_ripple_delete_inout_item_ = edit_menu_->AddItem("rippledeleteinout", nullptr, nullptr, "'"); edit_menu_->addSeparator(); - edit_set_marker_item_ = edit_menu_->AddItem("marker", nullptr, nullptr, "M"); + edit_set_marker_item_ = edit_menu_->AddItem("marker", this, SLOT(SetMarkerTriggered()), "M"); // // VIEW MENU @@ -100,9 +100,6 @@ MainMenu::MainMenu(QMainWindow *parent) : view_show_all_item_ = view_menu_->AddItem("showall", nullptr, nullptr, "\\"); view_show_all_item_->setCheckable(true); view_menu_->addSeparator(); - view_rectified_waveforms_item_ = view_menu_->AddItem("rectifiedwaveforms", nullptr, nullptr); - view_rectified_waveforms_item_->setCheckable(true); - view_menu_->addSeparator(); frame_view_mode_group_ = new QActionGroup(this); @@ -327,7 +324,7 @@ void MainMenu::TimecodeDisplayTriggered() Timecode::Display display = static_cast(action->data().toInt()); // Set the current display mode - Timecode::SetCurrentDisplay(display); + Core::instance()->SetTimecodeDisplay(display); } void MainMenu::FileMenuAboutToShow() @@ -343,7 +340,7 @@ void MainMenu::ViewMenuAboutToShow() // Ensure checked timecode display mode is correct QList timecode_display_actions = frame_view_mode_group_->actions(); foreach (QAction* a, timecode_display_actions) { - if (a->data() == Timecode::CurrentDisplay()) { + if (a->data() == Core::instance()->GetTimecodeDisplay()) { a->setChecked(true); break; } @@ -507,6 +504,11 @@ void MainMenu::GoToNextCutTriggered() PanelManager::instance()->CurrentlyFocused()->GoToNextCut(); } +void MainMenu::SetMarkerTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->SetMarker(); +} + void MainMenu::Retranslate() { // MenuShared is not a QWidget and therefore does not receive a LanguageEvent, we use MainMenu's to update it @@ -548,7 +550,6 @@ void MainMenu::Retranslate() view_increase_track_height_item_->setText(tr("Increase Track Height")); view_decrease_track_height_item_->setText(tr("Decrease Track Height")); view_show_all_item_->setText(tr("Toggle Show All")); - view_rectified_waveforms_item_->setText(tr("Rectified Waveforms")); view_timecode_view_frames_item_->setText(tr("Frames")); view_timecode_view_dropframe_item_->setText(tr("Drop Frame")); view_timecode_view_nondropframe_item_->setText(tr("Non-Drop Frame")); diff --git a/app/window/mainwindow/mainmenu.h b/app/window/mainwindow/mainmenu.h index 81476ab2d..d45b0818a 100644 --- a/app/window/mainwindow/mainmenu.h +++ b/app/window/mainwindow/mainmenu.h @@ -140,6 +140,8 @@ private slots: void GoToPrevCutTriggered(); void GoToNextCutTriggered(); + void SetMarkerTriggered(); + private: /** * @brief Set strings based on the current application language. @@ -179,7 +181,6 @@ private: QAction* view_increase_track_height_item_; QAction* view_decrease_track_height_item_; QAction* view_show_all_item_; - QAction* view_rectified_waveforms_item_; QActionGroup* frame_view_mode_group_; QAction* view_timecode_view_dropframe_item_; QAction* view_timecode_view_nondropframe_item_;